g360-cli 1.9.0 → 1.10.1
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/README.md +65 -7
- package/package.json +11 -2
- package/src/cli.js +54 -0
- package/src/commands/addon.js +188 -0
- package/src/commands/addon.test.js +37 -0
- package/src/commands/audit.test.js +32 -0
- package/src/commands/bring.test.js +37 -0
- package/src/commands/ingest.js +78 -84
- package/src/commands/init.js +11 -0
- package/src/commands/init.test.js +19 -0
- package/src/commands/list.test.js +132 -0
- package/src/commands/scan.js +68 -80
- package/src/commands/set-skill.js +21 -17
- package/src/commands/set-skill.test.js +134 -0
- package/src/commands/signature.js +46 -26
- package/src/commands/update.js +14 -2
- package/src/commands/validate.js +90 -66
- package/src/lib/asset-validator.test.js +237 -0
- package/src/lib/manifest.test.js +173 -0
- package/src/lib/python_runner.js +8 -8
- package/src/lib/validator.test.js +115 -0
- package/src/assets/engine/g360-data-validator.js +0 -44
- package/src/assets/engine/g360-engine.js +0 -12
- package/src/assets/engine/g360-field-mapper.js +0 -35
- package/src/assets/engine/g360-skill-audit.mjs +0 -37
- package/src/assets/engine/g360-skill-meta-evaluator.mjs +0 -33
- package/src/lib/assets.js +0 -38
- package/src/lib/checksum.js +0 -27
- package/src/lib/config.js +0 -23
- package/src/lib/offline.js +0 -33
- package/src/lib/presenter.js +0 -24
- package/src/lib/rollback.js +0 -49
- package/src/lib/theme.js +0 -30
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file asset-validator.test.js
|
|
3
|
+
* @description Tests para el módulo asset-validator
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
7
|
+
import fs from 'fs-extra';
|
|
8
|
+
import { assetValidator } from '../lib/asset-validator.js';
|
|
9
|
+
|
|
10
|
+
// Mock de fs-extra
|
|
11
|
+
vi.mock('fs-extra', () => ({
|
|
12
|
+
default: {
|
|
13
|
+
existsSync: vi.fn(),
|
|
14
|
+
readJson: vi.fn()
|
|
15
|
+
}
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
// Mock de logger
|
|
19
|
+
vi.mock('../lib/logger.js', () => ({
|
|
20
|
+
logger: {
|
|
21
|
+
debug: vi.fn(),
|
|
22
|
+
warn: vi.fn(),
|
|
23
|
+
error: vi.fn()
|
|
24
|
+
}
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
describe('asset-validator module', () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
vi.clearAllMocks();
|
|
30
|
+
assetValidator.clearCache();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('validateSkills', () => {
|
|
34
|
+
it('should validate valid skills data', async () => {
|
|
35
|
+
const validSkills = {
|
|
36
|
+
skills: [
|
|
37
|
+
{
|
|
38
|
+
name: 'corporativo',
|
|
39
|
+
description: 'Test skill',
|
|
40
|
+
device: 'pc',
|
|
41
|
+
colors: {
|
|
42
|
+
bg: '#0b1220',
|
|
43
|
+
surface: '#1a2332',
|
|
44
|
+
accent: '#00796B',
|
|
45
|
+
text: '#f0f4f8',
|
|
46
|
+
muted: '#94a3b8'
|
|
47
|
+
},
|
|
48
|
+
signature: {
|
|
49
|
+
mode: 'powered',
|
|
50
|
+
text: 'powered by G360'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
fs.existsSync.mockReturnValue(true);
|
|
57
|
+
fs.readJson.mockResolvedValue({
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
skills: {
|
|
61
|
+
type: 'array',
|
|
62
|
+
items: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
required: ['name', 'description', 'device', 'colors', 'signature'],
|
|
65
|
+
properties: {
|
|
66
|
+
name: { type: 'string' },
|
|
67
|
+
description: { type: 'string' },
|
|
68
|
+
device: { type: 'string', enum: ['pc', 'movil', 'both'] },
|
|
69
|
+
colors: { type: 'object' },
|
|
70
|
+
signature: { type: 'object' }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const result = await assetValidator.validateSkills(validSkills);
|
|
78
|
+
|
|
79
|
+
expect(result.valid).toBe(true);
|
|
80
|
+
expect(result.errors).toHaveLength(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should reject invalid skills data', async () => {
|
|
84
|
+
const invalidSkills = {
|
|
85
|
+
skills: [
|
|
86
|
+
{
|
|
87
|
+
name: 'test',
|
|
88
|
+
// Missing required fields
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
fs.existsSync.mockReturnValue(true);
|
|
94
|
+
fs.readJson.mockResolvedValue({
|
|
95
|
+
type: 'object',
|
|
96
|
+
properties: {
|
|
97
|
+
skills: {
|
|
98
|
+
type: 'array',
|
|
99
|
+
items: {
|
|
100
|
+
type: 'object',
|
|
101
|
+
required: ['name', 'description', 'device', 'colors', 'signature']
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const result = await assetValidator.validateSkills(invalidSkills);
|
|
108
|
+
|
|
109
|
+
expect(result.valid).toBe(false);
|
|
110
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('validateSnippets', () => {
|
|
115
|
+
it('should validate valid snippets data', async () => {
|
|
116
|
+
const validSnippets = {
|
|
117
|
+
snippets: [
|
|
118
|
+
{
|
|
119
|
+
name: 'test-snippet',
|
|
120
|
+
description: 'A test snippet',
|
|
121
|
+
code: 'console.log("test")'
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
fs.existsSync.mockReturnValue(true);
|
|
127
|
+
fs.readJson.mockResolvedValue({
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
snippets: {
|
|
131
|
+
type: 'array',
|
|
132
|
+
items: {
|
|
133
|
+
type: 'object',
|
|
134
|
+
required: ['name', 'description', 'code']
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const result = await assetValidator.validateSnippets(validSnippets);
|
|
141
|
+
|
|
142
|
+
expect(result.valid).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('validateAsset', () => {
|
|
147
|
+
it('should validate individual skill asset', async () => {
|
|
148
|
+
const skill = {
|
|
149
|
+
name: 'test-skill',
|
|
150
|
+
description: 'Test',
|
|
151
|
+
device: 'pc',
|
|
152
|
+
colors: { accent: '#00d084' },
|
|
153
|
+
signature: { mode: 'powered' }
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
fs.existsSync.mockReturnValue(true);
|
|
157
|
+
fs.readJson.mockResolvedValue({
|
|
158
|
+
type: 'object',
|
|
159
|
+
properties: {
|
|
160
|
+
skills: {
|
|
161
|
+
type: 'array',
|
|
162
|
+
items: { type: 'object' }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const result = await assetValidator.validateAsset('skill', skill);
|
|
168
|
+
|
|
169
|
+
expect(result).toHaveProperty('valid');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should validate individual snippet asset', async () => {
|
|
173
|
+
const snippet = {
|
|
174
|
+
name: 'test',
|
|
175
|
+
description: 'Test',
|
|
176
|
+
code: 'test'
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
fs.existsSync.mockReturnValue(true);
|
|
180
|
+
fs.readJson.mockResolvedValue({
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: {
|
|
183
|
+
snippets: {
|
|
184
|
+
type: 'array',
|
|
185
|
+
items: { type: 'object' }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const result = await assetValidator.validateAsset('snippet', snippet);
|
|
191
|
+
|
|
192
|
+
expect(result).toHaveProperty('valid');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('error handling', () => {
|
|
197
|
+
it('should handle missing schema file', async () => {
|
|
198
|
+
fs.existsSync.mockReturnValue(false);
|
|
199
|
+
|
|
200
|
+
const result = await assetValidator.validate('non-existent', {});
|
|
201
|
+
|
|
202
|
+
expect(result.valid).toBe(false);
|
|
203
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should handle invalid schema', async () => {
|
|
207
|
+
fs.existsSync.mockReturnValue(true);
|
|
208
|
+
fs.readJson.mockResolvedValue({ invalid: 'schema' });
|
|
209
|
+
|
|
210
|
+
const result = await assetValidator.validate('invalid-schema', {});
|
|
211
|
+
|
|
212
|
+
expect(result.valid).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('cache management', () => {
|
|
217
|
+
it('should cache compiled schemas', async () => {
|
|
218
|
+
fs.existsSync.mockReturnValue(true);
|
|
219
|
+
fs.readJson.mockResolvedValue({
|
|
220
|
+
type: 'object',
|
|
221
|
+
properties: {}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await assetValidator.validate('test', {});
|
|
225
|
+
await assetValidator.validate('test', {});
|
|
226
|
+
|
|
227
|
+
// Schema should be cached, so readJson should only be called once
|
|
228
|
+
expect(fs.readJson).toHaveBeenCalledTimes(1);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should clear cache when requested', async () => {
|
|
232
|
+
assetValidator.clearCache();
|
|
233
|
+
|
|
234
|
+
expect(assetValidator.schemaCache.size).toBe(0);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file manifest.test.js
|
|
3
|
+
* @description Tests para el módulo manifest
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
7
|
+
import fs from 'fs-extra';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { manifest } from '../lib/manifest.js';
|
|
10
|
+
|
|
11
|
+
// Mock de fs-extra
|
|
12
|
+
vi.mock('fs-extra', () => ({
|
|
13
|
+
default: {
|
|
14
|
+
writeJson: vi.fn(),
|
|
15
|
+
readJson: vi.fn(),
|
|
16
|
+
existsSync: vi.fn(),
|
|
17
|
+
remove: vi.fn()
|
|
18
|
+
}
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
describe('manifest module', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('init', () => {
|
|
27
|
+
it('should create a new manifest file', async () => {
|
|
28
|
+
const projectData = {
|
|
29
|
+
name: 'test-project',
|
|
30
|
+
template: 'web-pwa',
|
|
31
|
+
version: '1.0.0'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
fs.writeJson.mockResolvedValue();
|
|
35
|
+
|
|
36
|
+
const result = await manifest.init('/test/project', projectData);
|
|
37
|
+
|
|
38
|
+
expect(fs.writeJson).toHaveBeenCalledWith(
|
|
39
|
+
path.join('/test/project', 'g360-manifest.json'),
|
|
40
|
+
expect.objectContaining({
|
|
41
|
+
name: 'test-project',
|
|
42
|
+
template: 'web-pwa',
|
|
43
|
+
version: '1.0.0',
|
|
44
|
+
createdAt: expect.any(String),
|
|
45
|
+
assets: []
|
|
46
|
+
}),
|
|
47
|
+
{ spaces: 2 }
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
expect(result).toMatchObject({
|
|
51
|
+
name: 'test-project',
|
|
52
|
+
template: 'web-pwa',
|
|
53
|
+
version: '1.0.0'
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should include assets array in manifest', async () => {
|
|
58
|
+
const projectData = {
|
|
59
|
+
name: 'test-project',
|
|
60
|
+
template: 'web-pwa',
|
|
61
|
+
version: '1.0.0'
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
fs.writeJson.mockResolvedValue();
|
|
65
|
+
|
|
66
|
+
const result = await manifest.init('/test/project', projectData);
|
|
67
|
+
|
|
68
|
+
expect(result.assets).toEqual([]);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('load', () => {
|
|
73
|
+
it('should load existing manifest', async () => {
|
|
74
|
+
const mockManifest = {
|
|
75
|
+
name: 'test-project',
|
|
76
|
+
template: 'web-pwa',
|
|
77
|
+
version: '1.0.0',
|
|
78
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
79
|
+
assets: []
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
fs.existsSync.mockReturnValue(true);
|
|
83
|
+
fs.readJson.mockResolvedValue(mockManifest);
|
|
84
|
+
|
|
85
|
+
const result = await manifest.load('/test/project');
|
|
86
|
+
|
|
87
|
+
expect(result).toEqual(mockManifest);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should return null for non-existing manifest', async () => {
|
|
91
|
+
fs.existsSync.mockReturnValue(false);
|
|
92
|
+
|
|
93
|
+
const result = await manifest.load('/test/project');
|
|
94
|
+
|
|
95
|
+
expect(result).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should return null when manifest file does not exist', async () => {
|
|
99
|
+
fs.existsSync.mockReturnValue(true);
|
|
100
|
+
fs.readJson.mockRejectedValue(new Error('File not found'));
|
|
101
|
+
|
|
102
|
+
const result = await manifest.load('/test/project');
|
|
103
|
+
|
|
104
|
+
// El código actual no maneja errores de readJson, así que debería lanzar el error
|
|
105
|
+
// Para este test, vamos a verificar que el error se maneja correctamente
|
|
106
|
+
expect(result).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('addAsset', () => {
|
|
111
|
+
it('should add asset to existing manifest', async () => {
|
|
112
|
+
const existingManifest = {
|
|
113
|
+
name: 'test-project',
|
|
114
|
+
template: 'web-pwa',
|
|
115
|
+
version: '1.0.0',
|
|
116
|
+
assets: []
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const newAsset = {
|
|
120
|
+
name: 'components',
|
|
121
|
+
type: 'directory'
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
fs.readJson.mockResolvedValue(existingManifest);
|
|
125
|
+
fs.writeJson.mockResolvedValue();
|
|
126
|
+
|
|
127
|
+
await manifest.addAsset('/test/project', newAsset);
|
|
128
|
+
|
|
129
|
+
expect(fs.writeJson).toHaveBeenCalledWith(
|
|
130
|
+
path.join('/test/project', 'g360-manifest.json'),
|
|
131
|
+
expect.objectContaining({
|
|
132
|
+
assets: expect.arrayContaining([
|
|
133
|
+
expect.objectContaining({
|
|
134
|
+
name: 'components',
|
|
135
|
+
type: 'directory',
|
|
136
|
+
addedAt: expect.any(String)
|
|
137
|
+
})
|
|
138
|
+
])
|
|
139
|
+
}),
|
|
140
|
+
{ spaces: 2 }
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should not add asset when manifest does not exist', async () => {
|
|
145
|
+
fs.readJson.mockResolvedValue(null);
|
|
146
|
+
|
|
147
|
+
await manifest.addAsset('/test/project', { name: 'test' });
|
|
148
|
+
|
|
149
|
+
expect(fs.writeJson).not.toHaveBeenCalled();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('remove', () => {
|
|
154
|
+
it('should remove manifest file', async () => {
|
|
155
|
+
fs.existsSync.mockReturnValue(true);
|
|
156
|
+
fs.remove.mockResolvedValue();
|
|
157
|
+
|
|
158
|
+
await manifest.remove('/test/project');
|
|
159
|
+
|
|
160
|
+
expect(fs.remove).toHaveBeenCalledWith(
|
|
161
|
+
path.join('/test/project', 'g360-manifest.json')
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should handle non-existing manifest gracefully', async () => {
|
|
166
|
+
fs.existsSync.mockReturnValue(false);
|
|
167
|
+
|
|
168
|
+
await manifest.remove('/test/project');
|
|
169
|
+
|
|
170
|
+
expect(fs.remove).not.toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|
package/src/lib/python_runner.js
CHANGED
|
@@ -13,7 +13,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
13
13
|
/**
|
|
14
14
|
* Retorna la ruta al directorio src/g360_core del módulo Python.
|
|
15
15
|
*/
|
|
16
|
-
export function getPythonModulePath()
|
|
16
|
+
export function getPythonModulePath() {
|
|
17
17
|
// Ruta de desarrollo: py/src/g360_core relativo a este archivo
|
|
18
18
|
const devPath = path.join(__dirname, '..', 'py', 'src');
|
|
19
19
|
if (fs.existsSync(devPath)) {
|
|
@@ -39,7 +39,7 @@ export function getPythonModulePath(): string {
|
|
|
39
39
|
/**
|
|
40
40
|
* Genera código Python que configura sys.path correctamente.
|
|
41
41
|
*/
|
|
42
|
-
export function wrapPythonCode(pyCode
|
|
42
|
+
export function wrapPythonCode(pyCode) {
|
|
43
43
|
const modulePath = getPythonModulePath().replace(/\\/g, '\\\\');
|
|
44
44
|
|
|
45
45
|
return `
|
|
@@ -58,7 +58,7 @@ ${pyCode}
|
|
|
58
58
|
/**
|
|
59
59
|
* Ejecuta código Python y retorna su salida.
|
|
60
60
|
*/
|
|
61
|
-
export async function runPython(pyCode
|
|
61
|
+
export async function runPython(pyCode) {
|
|
62
62
|
const fullCode = wrapPythonCode(pyCode);
|
|
63
63
|
const pyExec = process.env.PYTHON || 'python3';
|
|
64
64
|
|
|
@@ -71,19 +71,19 @@ export async function runPython(pyCode: string): Promise<{ stdout: string; stder
|
|
|
71
71
|
let stdout = '';
|
|
72
72
|
let stderr = '';
|
|
73
73
|
|
|
74
|
-
await new Promise
|
|
75
|
-
proc.stdout?.on('data', (data
|
|
76
|
-
proc.stderr?.on('data', (data
|
|
74
|
+
await new Promise((resolve, reject) => {
|
|
75
|
+
proc.stdout?.on('data', (data) => { stdout += data.toString(); });
|
|
76
|
+
proc.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
77
77
|
|
|
78
78
|
proc.on('close', (code) => {
|
|
79
79
|
if (code === 0) resolve();
|
|
80
80
|
else reject(new Error(stderr || `Python código ${code}`));
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
-
proc.on('error', (err
|
|
83
|
+
proc.on('error', (err) => {
|
|
84
84
|
reject(new Error(`Python no disponible: ${err.message}`));
|
|
85
85
|
});
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
return { stdout, stderr };
|
|
89
|
-
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file validator.test.js
|
|
3
|
+
* @description Tests para el módulo validator
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
7
|
+
import fs from 'fs-extra';
|
|
8
|
+
import { validator } from '../lib/validator.js';
|
|
9
|
+
|
|
10
|
+
// Mock de fs-extra
|
|
11
|
+
vi.mock('fs-extra', () => ({
|
|
12
|
+
default: {
|
|
13
|
+
existsSync: vi.fn()
|
|
14
|
+
}
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
describe('validator module', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.clearAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('isValidProjectName', () => {
|
|
23
|
+
it('should accept valid project names', () => {
|
|
24
|
+
expect(validator.isValidProjectName('my-project')).toBe(true);
|
|
25
|
+
expect(validator.isValidProjectName('project123')).toBe(true);
|
|
26
|
+
expect(validator.isValidProjectName('my-project-123')).toBe(true);
|
|
27
|
+
expect(validator.isValidProjectName('123project')).toBe(true); // Empieza con número es válido según la regex actual
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should reject invalid project names', () => {
|
|
31
|
+
expect(validator.isValidProjectName('My-Project')).toBe(false); // Mayúsculas
|
|
32
|
+
expect(validator.isValidProjectName('my_project')).toBe(false); // Guiones bajos
|
|
33
|
+
expect(validator.isValidProjectName('my project')).toBe(false); // Espacios
|
|
34
|
+
expect(validator.isValidProjectName('-project')).toBe(false); // Empieza con guion
|
|
35
|
+
expect(validator.isValidProjectName('')).toBe(false); // Vacío
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('isValidPath', () => {
|
|
40
|
+
it('should return true for existing paths', () => {
|
|
41
|
+
fs.existsSync.mockReturnValue(true);
|
|
42
|
+
expect(validator.isValidPath('/existing/path')).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should return false for non-existing paths', () => {
|
|
46
|
+
fs.existsSync.mockReturnValue(false);
|
|
47
|
+
expect(validator.isValidPath('/non/existing/path')).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should handle errors gracefully', () => {
|
|
51
|
+
fs.existsSync.mockImplementation(() => {
|
|
52
|
+
throw new Error('Permission denied');
|
|
53
|
+
});
|
|
54
|
+
expect(validator.isValidPath('/error/path')).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('validateProject', () => {
|
|
59
|
+
it('should validate a valid G360 project', () => {
|
|
60
|
+
fs.existsSync.mockImplementation((path) => {
|
|
61
|
+
// El directorio del proyecto existe
|
|
62
|
+
if (path === '/valid/project') return true;
|
|
63
|
+
// El manifest y el directorio g360 existen
|
|
64
|
+
if (path.includes('g360-manifest.json') || path.includes('g360')) return true;
|
|
65
|
+
return false;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const result = validator.validateProject('/valid/project');
|
|
69
|
+
|
|
70
|
+
expect(result.valid).toBe(true);
|
|
71
|
+
expect(result.errors).toHaveLength(0);
|
|
72
|
+
expect(result.warnings).toHaveLength(0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should return errors for non-existing project', () => {
|
|
76
|
+
fs.existsSync.mockReturnValue(false);
|
|
77
|
+
|
|
78
|
+
const result = validator.validateProject('/non/existing');
|
|
79
|
+
|
|
80
|
+
expect(result.valid).toBe(false);
|
|
81
|
+
expect(result.errors).toContain('Project directory does not exist');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should return warnings for missing G360 files', () => {
|
|
85
|
+
fs.existsSync.mockImplementation((path) => {
|
|
86
|
+
// El directorio del proyecto existe
|
|
87
|
+
if (path === '/project/without/g360') return true;
|
|
88
|
+
// El manifest y el directorio g360 no existen
|
|
89
|
+
return false;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const result = validator.validateProject('/project/without/g360');
|
|
93
|
+
|
|
94
|
+
expect(result.valid).toBe(true);
|
|
95
|
+
expect(result.warnings).toContain('No g360-manifest.json found');
|
|
96
|
+
expect(result.warnings).toContain('No g360 directory found');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should handle partial G360 setup', () => {
|
|
100
|
+
fs.existsSync.mockImplementation((path) => {
|
|
101
|
+
// El directorio del proyecto existe
|
|
102
|
+
if (path === '/project/partial') return true;
|
|
103
|
+
// Solo el manifest existe
|
|
104
|
+
if (path.includes('g360-manifest.json')) return true;
|
|
105
|
+
// El directorio g360 no existe
|
|
106
|
+
return false;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const result = validator.validateProject('/project/partial');
|
|
110
|
+
|
|
111
|
+
expect(result.valid).toBe(true);
|
|
112
|
+
expect(result.warnings).toContain('No g360 directory found');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
export function validate(data, rules) {
|
|
2
|
-
const results = {
|
|
3
|
-
valid: true,
|
|
4
|
-
errors: [],
|
|
5
|
-
warnings: []
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
for (const [field, rule] of Object.entries(rules)) {
|
|
9
|
-
const value = data[field];
|
|
10
|
-
|
|
11
|
-
if (rule.required && (value === undefined || value === null || value === '')) {
|
|
12
|
-
results.errors.push({ field, message: `${field} is required` });
|
|
13
|
-
results.valid = false;
|
|
14
|
-
continue;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (rule.type && value !== undefined) {
|
|
18
|
-
const actualType = Array.isArray(value) ? 'array' : typeof value;
|
|
19
|
-
if (actualType !== rule.type) {
|
|
20
|
-
results.errors.push({ field, message: `${field} should be ${rule.type}` });
|
|
21
|
-
results.valid = false;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (rule.min !== undefined && typeof value === 'number' && value < rule.min) {
|
|
26
|
-
results.errors.push({ field, message: `${field} must be >= ${rule.min}` });
|
|
27
|
-
results.valid = false;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (rule.max !== undefined && typeof value === 'number' && value > rule.max) {
|
|
31
|
-
results.errors.push({ field, message: `${field} must be <= ${rule.max}` });
|
|
32
|
-
results.valid = false;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (rule.pattern && typeof value === 'string' && !rule.pattern.test(value)) {
|
|
36
|
-
results.errors.push({ field, message: `${field} format is invalid` });
|
|
37
|
-
results.valid = false;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return results;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export default { validate };
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
export function mapField(sourceField, fieldMap) {
|
|
2
|
-
const mapping = fieldMap[sourceField];
|
|
3
|
-
if (!mapping) {
|
|
4
|
-
return {
|
|
5
|
-
success: false,
|
|
6
|
-
targetField: null,
|
|
7
|
-
message: `No mapping found for: ${sourceField}`
|
|
8
|
-
};
|
|
9
|
-
}
|
|
10
|
-
return {
|
|
11
|
-
success: true,
|
|
12
|
-
targetField: mapping.target,
|
|
13
|
-
transformations: mapping.transform || []
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function validateMapping(sourceData, fieldMap) {
|
|
18
|
-
const results = {
|
|
19
|
-
valid: true,
|
|
20
|
-
errors: [],
|
|
21
|
-
warnings: []
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
for (const sourceField of Object.keys(sourceData)) {
|
|
25
|
-
const mapping = mapField(sourceField, fieldMap);
|
|
26
|
-
if (!mapping.success) {
|
|
27
|
-
results.warnings.push(mapping.message);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
results.valid = results.errors.length === 0;
|
|
32
|
-
return results;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export default { mapField, validateMapping };
|