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

1"""Sistema de validação pós-instalação para JTECH™ Core.""" 

2 

3import json 

4from dataclasses import dataclass 

5from pathlib import Path 

6from typing import Any, Dict, List, Optional 

7 

8import yaml 

9 

10from ..core.models import InstallationConfig, TeamType 

11 

12 

13@dataclass 

14class ValidationResult: 

15 """Resultado de uma validação específica.""" 

16 

17 component: str 

18 status: bool 

19 message: str 

20 details: Optional[Dict[str, Any]] = None 

21 

22 

23@dataclass 

24class ValidationReport: 

25 """Relatório completo de validação.""" 

26 

27 results: List[ValidationResult] 

28 

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) 

33 

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] 

38 

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 ] 

45 

46 @property 

47 def total_checks(self) -> int: 

48 """Total de verificações realizadas.""" 

49 return len(self.results) 

50 

51 @property 

52 def passed_checks(self) -> int: 

53 """Número de verificações que passaram.""" 

54 return len(self.successful_components) 

55 

56 @property 

57 def failed_checks(self) -> int: 

58 """Número de verificações que falharam.""" 

59 return len(self.failed_components) 

60 

61 

62class PostInstallationValidator: 

63 """Validador pós-instalação para verificar integridade e funcionalidade.""" 

64 

65 def __init__(self, config: InstallationConfig): 

66 """ 

67 Inicializa o validador. 

68 

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] = [] 

75 

76 def validate_all(self) -> ValidationReport: 

77 """ 

78 Executa todas as validações. 

79 

80 Returns: 

81 Relatório completo de validação 

82 """ 

83 self.results = [] 

84 

85 # Validações estruturais 

86 self._validate_directory_structure() 

87 self._validate_core_config() 

88 self._validate_vscode_configuration() 

89 

90 # Validações de conteúdo 

91 self._validate_agents() 

92 self._validate_chatmodes() 

93 self._validate_templates() 

94 

95 # Validações funcionais 

96 self._validate_file_permissions() 

97 self._validate_yaml_syntax() 

98 self._validate_json_syntax() 

99 

100 # Validações específicas por tipo de equipe 

101 self._validate_team_specific_setup() 

102 

103 # Gerar relatório 

104 return self._generate_report() 

105 

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 ] 

117 

118 if self.config.vs_code_integration: 

119 expected_dirs.append(".vscode") 

120 

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) 

126 

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 ) 

144 

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" 

148 

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 

158 

159 try: 

160 with open(config_file, "r", encoding="utf-8") as f: 

161 config_data = yaml.safe_load(f) 

162 

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 ] 

168 

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) 

181 

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 ) 

194 

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 ) 

211 

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 

223 

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 

234 

235 # Validar arquivos do VS Code 

236 vscode_files = ["settings.json", "extensions.json", "tasks.json"] 

237 issues = [] 

238 

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 

244 

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}") 

251 

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 ) 

269 

270 def _validate_agents(self) -> None: 

271 """Valida agentes instalados.""" 

272 agents_dir = self.project_path / ".jtech-core" / "agents" 

273 

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 

283 

284 # Mapear agentes esperados por tipo de equipe 

285 expected_agents = self._get_expected_agents() 

286 

287 installed_agents = list(agents_dir.glob("*.md")) 

288 installed_names = [agent.stem for agent in installed_agents] 

289 

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 ] 

296 

297 details = { 

298 "expected": expected_agents, 

299 "installed": installed_names, 

300 "missing": missing_agents, 

301 "extra": extra_agents, 

302 } 

303 

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 ) 

322 

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 ] 

329 

330 total_chatmodes = 0 

331 issues = [] 

332 

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 

339 

340 chatmode_files = list(chatmodes_dir.glob("*.chatmode.md")) 

341 total_chatmodes += len(chatmode_files) 

342 

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}") 

347 

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 ) 

369 

370 def _validate_templates(self) -> None: 

371 """Valida templates instalados.""" 

372 templates_dir = self.project_path / ".jtech-core" / "templates" 

373 

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 

383 

384 template_files = list(templates_dir.glob("*")) 

385 

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 ) 

403 

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 ] 

409 

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 ) 

417 

418 permission_issues = [] 

419 

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") 

428 

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 ) 

446 

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")) 

452 

453 syntax_errors = [] 

454 

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 

464 

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 ) 

482 

483 def _validate_json_syntax(self) -> None: 

484 """Valida sintaxe de todos os arquivos JSON.""" 

485 json_files = list(self.project_path.rglob("*.json")) 

486 

487 syntax_errors = [] 

488 

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 

498 

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 ) 

516 

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 

520 

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() 

533 

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) 

542 

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 ) 

568 

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 ) 

579 

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 ) 

590 

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 ) 

601 

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 } 

628 

629 return agent_mappings.get(self.config.team_type, []) 

630 

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 

634 

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 ) 

641 

642 return True 

643 

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() 

649 

650 # Validações básicas de formato 

651 if not content.strip(): 

652 return False 

653 

654 # Verificar se tem extensão correta 

655 if not chatmode_file.name.endswith(".chatmode.md"): 

656 return False 

657 

658 return True 

659 

660 except Exception: 

661 return False 

662 

663 def _generate_report(self) -> ValidationReport: 

664 """Gera relatório final de validação.""" 

665 return ValidationReport(results=self.results)