Coverage for src/jtech_installer/validator/post_installation.py: 20%
238 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-08-20 15:10 -0300
« prev ^ index » next coverage.py v7.8.0, created at 2025-08-20 15:10 -0300
1"""Sistema de validação pós-instalação para JTECH™ Core."""
3import json
4from dataclasses import dataclass
5from pathlib import Path
6from typing import Any, Dict, List, Optional
8import yaml
10from ..core.models import InstallationConfig, TeamType
13@dataclass
14class ValidationResult:
15 """Resultado de uma validação específica."""
17 component: str
18 status: bool
19 message: str
20 details: Optional[Dict[str, Any]] = None
23@dataclass
24class ValidationReport:
25 """Relatório completo de validação."""
27 results: List[ValidationResult]
29 @property
30 def is_valid(self) -> bool:
31 """Retorna True se todas as validações passaram."""
32 return all(result.status for result in self.results)
34 @property
35 def successful_components(self) -> List[str]:
36 """Lista de componentes que passaram na validação."""
37 return [result.component for result in self.results if result.status]
39 @property
40 def failed_components(self) -> List[str]:
41 """Lista de componentes que falharam na validação."""
42 return [
43 result.component for result in self.results if not result.status
44 ]
46 @property
47 def total_checks(self) -> int:
48 """Total de verificações realizadas."""
49 return len(self.results)
51 @property
52 def passed_checks(self) -> int:
53 """Número de verificações que passaram."""
54 return len(self.successful_components)
56 @property
57 def failed_checks(self) -> int:
58 """Número de verificações que falharam."""
59 return len(self.failed_components)
62class PostInstallationValidator:
63 """Validador pós-instalação para verificar integridade e funcionalidade."""
65 def __init__(self, config: InstallationConfig):
66 """
67 Inicializa o validador.
69 Args:
70 config: Configuração de instalação
71 """
72 self.config = config
73 self.project_path = config.project_path
74 self.results: List[ValidationResult] = []
76 def validate_all(self) -> ValidationReport:
77 """
78 Executa todas as validações.
80 Returns:
81 Relatório completo de validação
82 """
83 self.results = []
85 # Validações estruturais
86 self._validate_directory_structure()
87 self._validate_core_config()
88 self._validate_vscode_configuration()
90 # Validações de conteúdo
91 self._validate_agents()
92 self._validate_chatmodes()
93 self._validate_templates()
95 # Validações funcionais
96 self._validate_file_permissions()
97 self._validate_yaml_syntax()
98 self._validate_json_syntax()
100 # Validações específicas por tipo de equipe
101 self._validate_team_specific_setup()
103 # Gerar relatório
104 return self._generate_report()
106 def _validate_directory_structure(self) -> None:
107 """Valida a estrutura de diretórios."""
108 expected_dirs = [
109 ".jtech-core",
110 ".jtech-core/agents",
111 ".jtech-core/chatmodes",
112 ".jtech-core/templates",
113 ".jtech-core/tasks",
114 ".jtech-core/workflows",
115 ".github/chatmodes",
116 ]
118 if self.config.vs_code_integration:
119 expected_dirs.append(".vscode")
121 missing_dirs = []
122 for dir_path in expected_dirs:
123 full_path = self.project_path / dir_path
124 if not full_path.exists():
125 missing_dirs.append(dir_path)
127 if missing_dirs:
128 self.results.append(
129 ValidationResult(
130 component="directory_structure",
131 status=False,
132 message=f"Diretórios ausentes: {', '.join(missing_dirs)}",
133 details={"missing_directories": missing_dirs},
134 )
135 )
136 else:
137 self.results.append(
138 ValidationResult(
139 component="directory_structure",
140 status=True,
141 message="Estrutura de diretórios válida",
142 )
143 )
145 def _validate_core_config(self) -> None:
146 """Valida o arquivo core-config.yml."""
147 config_file = self.project_path / ".jtech-core" / "core-config.yml"
149 if not config_file.exists():
150 self.results.append(
151 ValidationResult(
152 component="core_config",
153 status=False,
154 message="Arquivo core-config.yml não encontrado",
155 )
156 )
157 return
159 try:
160 with open(config_file, "r", encoding="utf-8") as f:
161 config_data = yaml.safe_load(f)
163 # Validar campos obrigatórios
164 required_fields = ["slashPrefix", "prd", "architecture", "qa"]
165 missing_fields = [
166 field for field in required_fields if field not in config_data
167 ]
169 if missing_fields:
170 self.results.append(
171 ValidationResult(
172 component="core_config",
173 status=False,
174 message=f"Campos obrigatórios ausentes: {', '.join(missing_fields)}",
175 details={"missing_fields": missing_fields},
176 )
177 )
178 else:
179 # Validar configuração específica por tipo de equipe
180 team_valid = self._validate_team_config(config_data)
182 self.results.append(
183 ValidationResult(
184 component="core_config",
185 status=team_valid,
186 message=(
187 "Configuração core-config.yml válida"
188 if team_valid
189 else "Configuração específica da equipe inválida"
190 ),
191 details={"team_type": self.config.team_type.value},
192 )
193 )
195 except yaml.YAMLError as e:
196 self.results.append(
197 ValidationResult(
198 component="core_config",
199 status=False,
200 message=f"Erro de sintaxe YAML: {e}",
201 )
202 )
203 except Exception as e:
204 self.results.append(
205 ValidationResult(
206 component="core_config",
207 status=False,
208 message=f"Erro ao validar core-config.yml: {e}",
209 )
210 )
212 def _validate_vscode_configuration(self) -> None:
213 """Valida configurações do VS Code."""
214 if not self.config.vs_code_integration:
215 self.results.append(
216 ValidationResult(
217 component="vscode_config",
218 status=True,
219 message="Integração VS Code desabilitada",
220 )
221 )
222 return
224 vscode_dir = self.project_path / ".vscode"
225 if not vscode_dir.exists():
226 self.results.append(
227 ValidationResult(
228 component="vscode_config",
229 status=False,
230 message="Diretório .vscode não encontrado",
231 )
232 )
233 return
235 # Validar arquivos do VS Code
236 vscode_files = ["settings.json", "extensions.json", "tasks.json"]
237 issues = []
239 for file_name in vscode_files:
240 file_path = vscode_dir / file_name
241 if not file_path.exists():
242 issues.append(f"{file_name} ausente")
243 continue
245 # Validar JSON
246 try:
247 with open(file_path, "r", encoding="utf-8") as f:
248 json.load(f)
249 except json.JSONDecodeError as e:
250 issues.append(f"{file_name}: JSON inválido - {e}")
252 if issues:
253 self.results.append(
254 ValidationResult(
255 component="vscode_config",
256 status=False,
257 message=f"Problemas encontrados: {', '.join(issues)}",
258 details={"issues": issues},
259 )
260 )
261 else:
262 self.results.append(
263 ValidationResult(
264 component="vscode_config",
265 status=True,
266 message="Configuração VS Code válida",
267 )
268 )
270 def _validate_agents(self) -> None:
271 """Valida agentes instalados."""
272 agents_dir = self.project_path / ".jtech-core" / "agents"
274 if not agents_dir.exists():
275 self.results.append(
276 ValidationResult(
277 component="agents",
278 status=False,
279 message="Diretório de agentes não encontrado",
280 )
281 )
282 return
284 # Mapear agentes esperados por tipo de equipe
285 expected_agents = self._get_expected_agents()
287 installed_agents = list(agents_dir.glob("*.md"))
288 installed_names = [agent.stem for agent in installed_agents]
290 missing_agents = [
291 agent for agent in expected_agents if agent not in installed_names
292 ]
293 extra_agents = [
294 agent for agent in installed_names if agent not in expected_agents
295 ]
297 details = {
298 "expected": expected_agents,
299 "installed": installed_names,
300 "missing": missing_agents,
301 "extra": extra_agents,
302 }
304 if missing_agents:
305 self.results.append(
306 ValidationResult(
307 component="agents",
308 status=False,
309 message=f"Agentes ausentes: {', '.join(missing_agents)}",
310 details=details,
311 )
312 )
313 else:
314 self.results.append(
315 ValidationResult(
316 component="agents",
317 status=True,
318 message=f"Agentes validados: {len(installed_names)} instalados",
319 details=details,
320 )
321 )
323 def _validate_chatmodes(self) -> None:
324 """Valida chatmodes instalados."""
325 chatmodes_dirs = [
326 self.project_path / ".jtech-core" / "chatmodes",
327 self.project_path / ".github" / "chatmodes",
328 ]
330 total_chatmodes = 0
331 issues = []
333 for chatmodes_dir in chatmodes_dirs:
334 if not chatmodes_dir.exists():
335 issues.append(
336 f"Diretório {chatmodes_dir.relative_to(self.project_path)} não encontrado"
337 )
338 continue
340 chatmode_files = list(chatmodes_dir.glob("*.chatmode.md"))
341 total_chatmodes += len(chatmode_files)
343 # Validar formato dos arquivos
344 for chatmode_file in chatmode_files:
345 if not self._validate_chatmode_format(chatmode_file):
346 issues.append(f"Formato inválido: {chatmode_file.name}")
348 if issues:
349 self.results.append(
350 ValidationResult(
351 component="chatmodes",
352 status=False,
353 message=f"Problemas encontrados: {', '.join(issues)}",
354 details={
355 "total_chatmodes": total_chatmodes,
356 "issues": issues,
357 },
358 )
359 )
360 else:
361 self.results.append(
362 ValidationResult(
363 component="chatmodes",
364 status=True,
365 message=f"Chatmodes validados: {total_chatmodes} encontrados",
366 details={"total_chatmodes": total_chatmodes},
367 )
368 )
370 def _validate_templates(self) -> None:
371 """Valida templates instalados."""
372 templates_dir = self.project_path / ".jtech-core" / "templates"
374 if not templates_dir.exists():
375 self.results.append(
376 ValidationResult(
377 component="templates",
378 status=False,
379 message="Diretório de templates não encontrado",
380 )
381 )
382 return
384 template_files = list(templates_dir.glob("*"))
386 if not template_files:
387 self.results.append(
388 ValidationResult(
389 component="templates",
390 status=False,
391 message="Nenhum template encontrado",
392 )
393 )
394 else:
395 self.results.append(
396 ValidationResult(
397 component="templates",
398 status=True,
399 message=f"Templates validados: {len(template_files)} encontrados",
400 details={"template_count": len(template_files)},
401 )
402 )
404 def _validate_file_permissions(self) -> None:
405 """Valida permissões de arquivos."""
406 critical_files = [
407 self.project_path / ".jtech-core" / "core-config.yml"
408 ]
410 if self.config.vs_code_integration:
411 critical_files.extend(
412 [
413 self.project_path / ".vscode" / "settings.json",
414 self.project_path / ".vscode" / "extensions.json",
415 ]
416 )
418 permission_issues = []
420 for file_path in critical_files:
421 if file_path.exists():
422 if not file_path.is_file():
423 permission_issues.append(
424 f"{file_path.name} não é um arquivo"
425 )
426 elif not file_path.stat().st_mode & 0o444: # Readable
427 permission_issues.append(f"{file_path.name} não é legível")
429 if permission_issues:
430 self.results.append(
431 ValidationResult(
432 component="file_permissions",
433 status=False,
434 message=f"Problemas de permissão: {', '.join(permission_issues)}",
435 details={"issues": permission_issues},
436 )
437 )
438 else:
439 self.results.append(
440 ValidationResult(
441 component="file_permissions",
442 status=True,
443 message="Permissões de arquivo válidas",
444 )
445 )
447 def _validate_yaml_syntax(self) -> None:
448 """Valida sintaxe de todos os arquivos YAML."""
449 yaml_files = []
450 yaml_files.extend(self.project_path.rglob("*.yml"))
451 yaml_files.extend(self.project_path.rglob("*.yaml"))
453 syntax_errors = []
455 for yaml_file in yaml_files:
456 try:
457 with open(yaml_file, "r", encoding="utf-8") as f:
458 yaml.safe_load(f)
459 except yaml.YAMLError as e:
460 syntax_errors.append(f"{yaml_file.name}: {e}")
461 except Exception:
462 # Pular arquivos que não conseguimos ler
463 continue
465 if syntax_errors:
466 self.results.append(
467 ValidationResult(
468 component="yaml_syntax",
469 status=False,
470 message=f"Erros de sintaxe YAML: {', '.join(syntax_errors)}",
471 details={"errors": syntax_errors},
472 )
473 )
474 else:
475 self.results.append(
476 ValidationResult(
477 component="yaml_syntax",
478 status=True,
479 message=f"Sintaxe YAML válida em {len(yaml_files)} arquivos",
480 )
481 )
483 def _validate_json_syntax(self) -> None:
484 """Valida sintaxe de todos os arquivos JSON."""
485 json_files = list(self.project_path.rglob("*.json"))
487 syntax_errors = []
489 for json_file in json_files:
490 try:
491 with open(json_file, "r", encoding="utf-8") as f:
492 json.load(f)
493 except json.JSONDecodeError as e:
494 syntax_errors.append(f"{json_file.name}: {e}")
495 except Exception:
496 # Pular arquivos que não conseguimos ler
497 continue
499 if syntax_errors:
500 self.results.append(
501 ValidationResult(
502 component="json_syntax",
503 status=False,
504 message=f"Erros de sintaxe JSON: {', '.join(syntax_errors)}",
505 details={"errors": syntax_errors},
506 )
507 )
508 else:
509 self.results.append(
510 ValidationResult(
511 component="json_syntax",
512 status=True,
513 message=f"Sintaxe JSON válida em {len(json_files)} arquivos",
514 )
515 )
517 def _validate_team_specific_setup(self) -> None:
518 """Valida configurações específicas do tipo de equipe."""
519 team_type = self.config.team_type
521 if team_type == TeamType.IDE_MINIMAL:
522 # Validar configuração mínima
523 self._validate_minimal_setup()
524 elif team_type == TeamType.FULLSTACK:
525 # Validar configuração full-stack
526 self._validate_fullstack_setup()
527 elif team_type == TeamType.NO_UI:
528 # Validar configuração backend
529 self._validate_backend_setup()
530 elif team_type == TeamType.ALL:
531 # Validar configuração completa
532 self._validate_complete_setup()
534 def _validate_minimal_setup(self) -> None:
535 """Valida configuração IDE minimal."""
536 if self.config.vs_code_integration:
537 settings_file = self.project_path / ".vscode" / "settings.json"
538 if settings_file.exists():
539 try:
540 with open(settings_file, "r", encoding="utf-8") as f:
541 settings = json.load(f)
543 # Verificar configurações específicas minimal
544 if settings.get("editor.minimap.enabled") is False:
545 self.results.append(
546 ValidationResult(
547 component="team_setup_minimal",
548 status=True,
549 message="Configuração IDE minimal validada",
550 )
551 )
552 else:
553 self.results.append(
554 ValidationResult(
555 component="team_setup_minimal",
556 status=False,
557 message="Configurações IDE minimal não aplicadas",
558 )
559 )
560 except Exception as e:
561 self.results.append(
562 ValidationResult(
563 component="team_setup_minimal",
564 status=False,
565 message=f"Erro ao validar configuração minimal: {e}",
566 )
567 )
569 def _validate_fullstack_setup(self) -> None:
570 """Valida configuração fullstack."""
571 # Implementar validações específicas fullstack
572 self.results.append(
573 ValidationResult(
574 component="team_setup_fullstack",
575 status=True,
576 message="Configuração fullstack validada",
577 )
578 )
580 def _validate_backend_setup(self) -> None:
581 """Valida configuração backend/no-ui."""
582 # Implementar validações específicas backend
583 self.results.append(
584 ValidationResult(
585 component="team_setup_backend",
586 status=True,
587 message="Configuração backend validada",
588 )
589 )
591 def _validate_complete_setup(self) -> None:
592 """Valida configuração completa."""
593 # Implementar validações específicas completa
594 self.results.append(
595 ValidationResult(
596 component="team_setup_complete",
597 status=True,
598 message="Configuração completa validada",
599 )
600 )
602 def _get_expected_agents(self) -> List[str]:
603 """Retorna lista de agentes esperados por tipo de equipe."""
604 agent_mappings = {
605 TeamType.IDE_MINIMAL: ["pm", "architect", "dev"],
606 TeamType.FULLSTACK: [
607 "pm",
608 "architect",
609 "dev",
610 "qa",
611 "ui",
612 "fullstack",
613 ],
614 TeamType.NO_UI: ["pm", "architect", "dev", "qa", "backend"],
615 TeamType.ALL: [
616 "pm",
617 "architect",
618 "dev",
619 "qa",
620 "ui",
621 "fullstack",
622 "backend",
623 "devops",
624 "security",
625 "data",
626 ],
627 }
629 return agent_mappings.get(self.config.team_type, [])
631 def _validate_team_config(self, config_data: Dict[str, Any]) -> bool:
632 """Valida configuração específica por tipo de equipe."""
633 team_type = self.config.team_type
635 if team_type == TeamType.IDE_MINIMAL:
636 return config_data.get("customTechnicalDocuments") is None
637 elif team_type in [TeamType.FULLSTACK, TeamType.NO_UI, TeamType.ALL]:
638 return isinstance(
639 config_data.get("customTechnicalDocuments"), list
640 )
642 return True
644 def _validate_chatmode_format(self, chatmode_file: Path) -> bool:
645 """Valida formato de um arquivo chatmode."""
646 try:
647 with open(chatmode_file, "r", encoding="utf-8") as f:
648 content = f.read()
650 # Validações básicas de formato
651 if not content.strip():
652 return False
654 # Verificar se tem extensão correta
655 if not chatmode_file.name.endswith(".chatmode.md"):
656 return False
658 return True
660 except Exception:
661 return False
663 def _generate_report(self) -> ValidationReport:
664 """Gera relatório final de validação."""
665 return ValidationReport(results=self.results)