mirror of
https://github.com/Pandipipas/scoreko-electron-dev.git
synced 2026-06-05 21:22:07 +00:00
Merge pull request #30 from Pandipipas/translate-all-spanish-text-to-english
Translate Spanish content to English across docs, scripts, runtime messages, and tests
This commit is contained in:
@@ -1,20 +1,20 @@
|
|||||||
# scoreko-electron-dev
|
# scoreko-electron-dev
|
||||||
|
|
||||||
Wrapper de Electron para empaquetar una instalación de NodeCG que incluya el bundle `scoreko-dev`, inspirado en `opeik/runback-electron` pero actualizado a Electron + TypeScript moderno.
|
Electron wrapper to package a NodeCG installation that includes the `scoreko-dev` bundle, inspired by `opeik/runback-electron` but updated to modern Electron + TypeScript.
|
||||||
|
|
||||||
## Requisitos clave
|
## Key requirements
|
||||||
|
|
||||||
- Node `>=22` (`.nvmrc` incluido).
|
- Node `>=22` (`.nvmrc` included).
|
||||||
- Electron fijado en `39.5.1`.
|
- Electron pinned to `39.5.1`.
|
||||||
|
|
||||||
## Qué hace
|
## What it does
|
||||||
|
|
||||||
- Arranca `lib/nodecg/index.js` como proceso hijo desde Electron.
|
- Starts `lib/nodecg/index.js` as a child process from Electron.
|
||||||
- Muestra la ruta de dashboard de carga del bundle (`/bundles/<bundle>/dashboard/loading.html`) servida por NodeCG mientras inicia.
|
- Shows the bundle loading dashboard route (`/bundles/<bundle>/dashboard/loading.html`) served by NodeCG while it starts.
|
||||||
- Carga el dashboard del bundle en `http://localhost:<puerto>/bundles/<bundle>/<ruta-dashboard>`.
|
- Loads the bundle dashboard at `http://localhost:<port>/bundles/<bundle>/<dashboard-route>`.
|
||||||
- Empaqueta NodeCG + assets dentro de la app final con `electron-builder`.
|
- Packages NodeCG + assets inside the final app with `electron-builder`.
|
||||||
|
|
||||||
## Estructura esperada
|
## Expected structure
|
||||||
|
|
||||||
```text
|
```text
|
||||||
scoreko-electron-dev/
|
scoreko-electron-dev/
|
||||||
@@ -31,34 +31,34 @@ scoreko-electron-dev/
|
|||||||
|
|
||||||
## Scripts
|
## Scripts
|
||||||
|
|
||||||
- `npm run dev`: modo desarrollo.
|
- `npm run dev`: development mode.
|
||||||
- `npm run build`: compila TypeScript y copia assets.
|
- `npm run build`: compile TypeScript and copy assets.
|
||||||
- `npm run start`: build y ejecución local.
|
- `npm run start`: build and local run.
|
||||||
- `npm run test`: build + tests unitarios (`node:test`).
|
- `npm run test`: build + unit tests (`node:test`).
|
||||||
- `npm run doctor`: preflight de configuración y entorno (`lib/nodecg`, permisos, puerto y env vars).
|
- `npm run doctor`: configuration/environment preflight (`lib/nodecg`, permissions, port, and env vars).
|
||||||
- `npm run lint`: reglas mínimas de calidad con ESLint.
|
- `npm run lint`: minimal quality rules with ESLint.
|
||||||
- `npm run format`: validación de formato con Prettier.
|
- `npm run format`: format validation with Prettier.
|
||||||
- `npm run pack`: genera app sin instalador.
|
- `npm run pack`: generate app without installer.
|
||||||
- `npm run dist`: genera instalador.
|
- `npm run dist`: generate installer.
|
||||||
|
|
||||||
## Variables de entorno
|
## Environment variables
|
||||||
|
|
||||||
La **fuente única de defaults** está en `.env.example`.
|
The **single source of truth for defaults** is in `.env.example`.
|
||||||
|
|
||||||
1. Copia `.env.example` a `.env` (o exporta variables en tu shell/CI).
|
1. Copy `.env.example` to `.env` (or export variables in your shell/CI).
|
||||||
2. Ajusta sólo lo necesario.
|
2. Adjust only what you need.
|
||||||
3. Ejecuta `npm run doctor` para validar configuración antes de arrancar.
|
3. Run `npm run doctor` to validate configuration before starting.
|
||||||
|
|
||||||
## Build multi-plataforma (iconos)
|
## Cross-platform build (icons)
|
||||||
|
|
||||||
- `build.win.icon`: `static/icons/icon.ico`
|
- `build.win.icon`: `static/icons/icon.ico`
|
||||||
- `build.linux.icon`: `static/icons`
|
- `build.linux.icon`: `static/icons`
|
||||||
- `build.mac.icon`: `static/icons/icon.icns`
|
- `build.mac.icon`: `static/icons/icon.icns`
|
||||||
|
|
||||||
> El `.icns` se referencia en la configuración de build y debe existir localmente para empaquetar macOS.
|
> The `.icns` is referenced in build config and must exist locally to package macOS.
|
||||||
|
|
||||||
## Troubleshooting y arquitectura
|
## Troubleshooting and architecture
|
||||||
|
|
||||||
- Guía de troubleshooting: `docs/troubleshooting.md`
|
- Troubleshooting guide: `docs/troubleshooting.md`
|
||||||
- Mapa de arquitectura: `docs/architecture.md`
|
- Architecture map: `docs/architecture.md`
|
||||||
- Roadmap: `docs/refactor-roadmap.md`
|
- Roadmap: `docs/refactor-roadmap.md`
|
||||||
|
|||||||
+18
-18
@@ -1,24 +1,24 @@
|
|||||||
# Arquitectura del proceso principal
|
# Main process architecture
|
||||||
|
|
||||||
## Flujo de arranque
|
## Startup flow
|
||||||
|
|
||||||
1. `src/main/main.ts` carga `appConfig` desde `config/runtime-config.ts`.
|
1. `src/main/main.ts` loads `appConfig` from `config/runtime-config.ts`.
|
||||||
2. Crea ventanas (`windows/window-factory.ts`).
|
2. Creates windows (`windows/window-factory.ts`).
|
||||||
3. Arranca NodeCG con `nodecg/process-manager.ts`.
|
3. Starts NodeCG with `nodecg/process-manager.ts`.
|
||||||
4. Espera readiness HTTP y muestra loading -> dashboard principal.
|
4. Waits for HTTP readiness and shows loading -> main dashboard.
|
||||||
5. En cierre, ejecuta un único flujo de stop graceful para evitar procesos huérfanos.
|
5. On shutdown, runs a single graceful-stop flow to avoid orphan processes.
|
||||||
|
|
||||||
## Módulos principales
|
## Main modules
|
||||||
|
|
||||||
- `config/runtime-config.ts`: lectura/validación de env vars.
|
- `config/runtime-config.ts`: read/validate env vars.
|
||||||
- `nodecg/process-manager.ts`: start, readiness y stop de NodeCG, validaciones de instalación/permisos/puerto.
|
- `nodecg/process-manager.ts`: start, readiness, and stop for NodeCG; install/permission/port validation.
|
||||||
- `windows/window-factory.ts`: creación de ventanas y política de navegación.
|
- `windows/window-factory.ts`: window creation and navigation policy.
|
||||||
- `windows/navigation-security.ts`: allowlist de navegación interna y esquemas externos seguros.
|
- `windows/navigation-security.ts`: internal navigation allowlist and safe external schemes.
|
||||||
- `errors/error-presenter.ts`: presentación de errores fatales.
|
- `errors/error-presenter.ts`: fatal error presentation.
|
||||||
- `errors/logger.ts`: logging estructurado (`info/warn/error/debug`).
|
- `errors/logger.ts`: structured logging (`info/warn/error/debug`).
|
||||||
|
|
||||||
## Principios
|
## Principles
|
||||||
|
|
||||||
- Refactors mecánicos primero.
|
- Mechanical refactors first.
|
||||||
- Hardening incremental con fallback conservador.
|
- Incremental hardening with conservative fallback.
|
||||||
- Validación automática por `typecheck`, `build`, `test`, `doctor`, `lint`.
|
- Automated validation via `typecheck`, `build`, `test`, `doctor`, `lint`.
|
||||||
|
|||||||
+129
-149
@@ -1,215 +1,195 @@
|
|||||||
# Roadmap de refactor, limpieza y mejoras (sin romper funcionalidad)
|
# Refactor, cleanup, and improvement roadmap (without breaking functionality)
|
||||||
|
|
||||||
Este documento detalla una propuesta de mejoras para `scoreko-electron-dev` priorizando **cero regresiones**. Cada iniciativa incluye objetivo, acciones concretas y estrategia de implementación segura.
|
This document outlines an improvement proposal for `scoreko-electron-dev`, prioritizing **zero regressions**. Each initiative includes a goal, concrete actions, and a safe implementation strategy.
|
||||||
|
|
||||||
## 1) Arquitectura y organización del código
|
## 1) Architecture and code organization
|
||||||
|
|
||||||
### 1.1 Separar `main.ts` por responsabilidades
|
### 1.1 Split `main.ts` responsibilities
|
||||||
|
|
||||||
- **Problema actual:** `src/main/main.ts` concentra configuración, UI, lifecycle, gestión de procesos, manejo de errores y utilidades.
|
- **Current issue:** `src/main/main.ts` mixes configuration, UI, lifecycle, process management, error handling, and utilities.
|
||||||
- **Acciones:**
|
- **Actions:**
|
||||||
- Crear `src/main/config/runtime-config.ts` para parseo de env vars.
|
- Create `src/main/config/runtime-config.ts` for env var parsing.
|
||||||
- Crear `src/main/nodecg/process-manager.ts` para `start/stop/wait-ready`.
|
- Create `src/main/nodecg/process-manager.ts` for `start/stop/wait-ready`.
|
||||||
- Crear `src/main/windows/window-factory.ts` para creación de ventanas.
|
- Create `src/main/windows/window-factory.ts` for window creation.
|
||||||
- Crear `src/main/errors/error-presenter.ts` para logging + `dialog.showErrorBox`.
|
- Create `src/main/errors/error-presenter.ts` for logging + `dialog.showErrorBox`.
|
||||||
- **Riesgo:** bajo, si se conserva API interna por pasos.
|
- **Safe implementation:** move functions without changing behavior, cover with unit tests before modifying logic.
|
||||||
- **Implementación segura:** mover funciones sin cambiar lógica, cubrir con tests unitarios antes de modificar comportamiento.
|
|
||||||
|
|
||||||
### 1.2 Consolidar constantes de runtime
|
### 1.2 Consolidate runtime constants
|
||||||
|
|
||||||
- **Problema actual:** constantes de URLs y tamaños están dispersas.
|
- **Current issue:** URL and size constants are scattered.
|
||||||
- **Acciones:** agrupar en `src/main/constants.ts` y tiparlas con `as const`.
|
- **Action:** group them in `src/main/constants.ts` and type with `as const`.
|
||||||
- **Beneficio:** facilita ajuste de defaults y revisiones.
|
- **Benefit:** easier default tuning and review.
|
||||||
|
|
||||||
### 1.3 Normalizar naming interno
|
### 1.3 Improve naming
|
||||||
|
|
||||||
- **Mejoras propuestas:**
|
- **Goal:** more semantic names for maintainability.
|
||||||
- `runtimeConfig` → `appConfig`.
|
|
||||||
- `nodecgPath` → `nodecgRootPath`.
|
|
||||||
- `RUNTIME_NAME` → `NODE_RUNTIME_NAME`.
|
|
||||||
- **Objetivo:** nombres más semánticos para mantenimiento.
|
|
||||||
|
|
||||||
## 2) Robustez de proceso NodeCG
|
## 2) NodeCG process robustness
|
||||||
|
|
||||||
### 2.1 Endurecer validaciones de instalación
|
### 2.1 Harden install validations
|
||||||
|
|
||||||
- **Mejoras:**
|
- **Actions:**
|
||||||
- Validar permisos de lectura/escritura en `lib/nodecg`.
|
- Validate read/write permissions in `lib/nodecg`.
|
||||||
- Validar si el puerto está ocupado antes de lanzar.
|
- Validate whether the port is occupied before launching.
|
||||||
- Incluir diagnóstico más accionable en mensajes de error (comando sugerido exacto).
|
- Include more actionable diagnostics in error messages (exact suggested command).
|
||||||
- **Implementación segura:** agregar validaciones opt-in primero con logs, luego convertir a hard-fail.
|
- **Safe implementation:** add opt-in validations with logs first, then convert to hard-fail.
|
||||||
|
|
||||||
### 2.2 Mejorar el check de “ready”
|
### 2.2 Improve the "ready" check
|
||||||
|
|
||||||
- **Problema:** readiness actual con `GET /` + `response.ok || 404` puede dar falsos positivos.
|
- **Current issue:** readiness with `GET /` + `response.ok || 404` can give false positives.
|
||||||
- **Acciones:**
|
- **Actions:**
|
||||||
- Intentar endpoint más explícito de NodeCG si existe.
|
- Try a more explicit NodeCG endpoint if available.
|
||||||
- Si no existe, añadir secuencia de checks con retries exponenciales + jitter.
|
- Otherwise add a check sequence with exponential retries + jitter.
|
||||||
- **Compatibilidad:** mantener fallback al check actual inicialmente.
|
|
||||||
|
|
||||||
### 2.3 Control del shutdown más determinista
|
### 2.3 More deterministic shutdown control
|
||||||
|
|
||||||
- **Acciones:**
|
- **Actions:**
|
||||||
- Añadir estado explícito (`starting/running/stopping/stopped`).
|
- Add explicit state (`starting/running/stopping/stopped`).
|
||||||
- Evitar dobles señales en hooks `before-quit`, `will-quit`, `process.exit`.
|
- Avoid duplicate signals in `before-quit`, `will-quit`, and `process.exit` hooks.
|
||||||
- Registrar duración de parada para diagnósticos.
|
- Record stop duration for diagnostics.
|
||||||
|
|
||||||
## 3) Ventanas Electron y UX de carga
|
## 3) Electron windows and loading UX
|
||||||
|
|
||||||
### 3.1 Rework de loading flow
|
### 3.1 Rework loading flow
|
||||||
|
|
||||||
- **Mejoras:**
|
- **Actions:**
|
||||||
- Evitar cargar la ventana principal demasiado pronto si falla dashboard.
|
- Avoid loading the main window too early if the dashboard fails.
|
||||||
- Añadir timeout específico para `mainWindow.loadURL`.
|
- Add a specific timeout for `mainWindow.loadURL`.
|
||||||
- Incluir fallback de pantalla de error amigable dentro de Electron.
|
- Include a friendly error-screen fallback inside Electron.
|
||||||
|
|
||||||
### 3.2 Seguridad de navegación
|
### 3.2 Navigation security
|
||||||
|
|
||||||
- **Acciones:**
|
- **Actions:**
|
||||||
- Validar que `setWindowOpenHandler` y `will-navigate` permitan solo dominio esperado (`localhost:PORT`).
|
- Validate that `setWindowOpenHandler` and `will-navigate` only allow the expected domain (`localhost:PORT`).
|
||||||
- Rechazar esquemas inseguros (`file:`, `javascript:`).
|
- Keep external links in the default browser.
|
||||||
- **Beneficio:** hardening del proceso principal.
|
|
||||||
|
|
||||||
### 3.3 Ajustes de resolución
|
### 3.3 Resolution settings
|
||||||
|
|
||||||
- **Mejora:** no fijar `minWidth/minHeight` a 1920x1080 en todos los escenarios.
|
- **Improvement:** do not fix `minWidth/minHeight` at 1920x1080 for all scenarios.
|
||||||
- **Propuesta segura:** usar valores por env var con defaults actuales para mantener compatibilidad.
|
- **Safe proposal:** use env-var values with current defaults for compatibility.
|
||||||
|
|
||||||
## 4) Configuración y variables de entorno
|
## 4) Configuration and environment variables
|
||||||
|
|
||||||
### 4.1 Validación tipada de env vars
|
### 4.1 Typed env var validation
|
||||||
|
|
||||||
- **Acciones:**
|
- **Actions:**
|
||||||
- Introducir esquema (`zod` o validación manual centralizada).
|
- Introduce a schema (`zod` or centralized manual validation).
|
||||||
- Rechazar puertos fuera de rango.
|
- Reject out-of-range ports.
|
||||||
- Marcar strings vacíos inválidos en rutas críticas.
|
- Treat empty strings as invalid in critical paths.
|
||||||
|
|
||||||
### 4.2 Documentación ejecutable
|
### 4.2 Executable documentation
|
||||||
|
|
||||||
- **Acciones:**
|
- **Actions:**
|
||||||
- Añadir `.env.example` con todos los defaults.
|
- Add `.env.example` with all defaults.
|
||||||
- Script de validación `npm run doctor` para detectar configuración inválida.
|
- Add `npm run doctor` validation script to detect invalid configuration.
|
||||||
|
|
||||||
### 4.3 Unificar defaults entre código y README
|
### 4.3 Unify defaults between code and README
|
||||||
|
|
||||||
- **Problema:** existe posible drift entre doc y runtime.
|
- **Current issue:** docs and runtime can drift.
|
||||||
- **Acción:** generar bloque de README automáticamente desde una fuente única de config.
|
- **Action:** generate the README block automatically from a single config source.
|
||||||
|
|
||||||
## 5) Build, packaging y distribución
|
## 5) Build, packaging, and distribution
|
||||||
|
|
||||||
### 5.1 Revisar iconos por plataforma
|
### 5.1 Platform icon hardening
|
||||||
|
|
||||||
- **Problema:** en `build.mac.icon` se usa `.ico` (no ideal para macOS).
|
- **Current issue:** `build.mac.icon` uses `.ico` (not ideal for macOS).
|
||||||
- **Acciones:**
|
- **Actions:**
|
||||||
- Usar `.icns` en macOS.
|
- Use `.icns` for macOS.
|
||||||
- Mantener `.ico` en Windows y set icon set correcto para Linux.
|
- Keep `.ico` on Windows and set the correct icon set for Linux.
|
||||||
- **Estrategia segura:** fallback conservando lo actual hasta tener assets definitivos.
|
|
||||||
|
|
||||||
### 5.2 Pipeline reproducible
|
### 5.2 Reproducible pipeline
|
||||||
|
|
||||||
- **Acciones:**
|
- **Actions:**
|
||||||
- Asegurar lockfile limpio y versión de Node fijada con `.nvmrc`.
|
- Ensure a clean lockfile and pinned Node version with `.nvmrc`.
|
||||||
- Añadir CI mínima (`typecheck`, build, smoke test).
|
- Add minimal CI (`typecheck`, `build`, smoke test).
|
||||||
|
|
||||||
### 5.3 Reducción de tamaño de artefacto
|
### 5.3 Artifact size reduction
|
||||||
|
|
||||||
- **Acciones:**
|
- **Actions:**
|
||||||
- Revisar qué se copia en `extraResources`.
|
- Review what is copied in `extraResources`.
|
||||||
- Excluir archivos no necesarios (logs, tests, cachés) en empaquetado.
|
- Exclude unnecessary files (logs, tests, caches) in packaging.
|
||||||
|
|
||||||
## 6) Calidad de código y testing
|
## 6) Code quality and testing
|
||||||
|
|
||||||
### 6.1 Añadir linting/formatting
|
### 6.1 Add linting/formatting
|
||||||
|
|
||||||
- **Acciones:**
|
- **Actions:**
|
||||||
- Configurar ESLint + Prettier.
|
- ESLint + Prettier in scripts and CI.
|
||||||
- Reglas mínimas: imports ordenados, no variables no usadas, complejidad controlada.
|
- Minimum rules: sorted imports, no unused variables, controlled complexity.
|
||||||
|
|
||||||
### 6.2 Unit tests para utilidades críticas
|
### 6.2 Unit tests for critical utilities
|
||||||
|
|
||||||
- **Cobertura objetivo inicial:**
|
- **Coverage target:**
|
||||||
- `parseEnvInt`, `getEnv`, `getOptionalEnv`.
|
- Env var parsing.
|
||||||
- Resolución de icon path.
|
- Navigation allowlist.
|
||||||
- Cálculo de delays y timeouts.
|
- Icon path resolution.
|
||||||
|
- Delay and timeout calculation.
|
||||||
|
|
||||||
### 6.3 Integration smoke tests
|
### 6.3 Main bootstrap smoke tests
|
||||||
|
|
||||||
- **Acción:** test que verifique arranque controlado de Electron main con mocks (sin UI real).
|
- **Action:** test controlled Electron main startup with mocks (no real UI).
|
||||||
- **Objetivo:** detectar regresiones de lifecycle y cierre de NodeCG.
|
|
||||||
|
|
||||||
## 7) Observabilidad y diagnóstico
|
## 7) Observability and diagnostics
|
||||||
|
|
||||||
### 7.1 Logger estructurado
|
### 7.1 Structured logging
|
||||||
|
|
||||||
- **Acción:** reemplazar `console.log` por logger con niveles (`info/warn/error/debug`) y contexto.
|
- **Action:** replace `console.log` with leveled logger (`info/warn/error/debug`) and context.
|
||||||
- **Beneficio:** depuración más rápida en producción.
|
- **Benefit:** faster production debugging.
|
||||||
|
|
||||||
### 7.2 Error codes y troubleshooting
|
### 7.2 Error taxonomy
|
||||||
|
|
||||||
- **Acciones:**
|
- **Actions:**
|
||||||
- Estandarizar errores con códigos (`E_NODECG_NOT_FOUND`, etc.).
|
- Standardize errors with codes (`E_NODECG_NOT_FOUND`, etc.).
|
||||||
- Añadir sección “Troubleshooting” en README con causas/soluciones.
|
- Add a README troubleshooting section with causes/solutions.
|
||||||
|
|
||||||
## 8) Limpieza técnica (deuda)
|
## 8) Technical cleanup (debt)
|
||||||
|
|
||||||
### 8.1 Eliminar lógica duplicada de cierre
|
### 8.1 Remove duplicated shutdown logic
|
||||||
|
|
||||||
- **Problema:** varios handlers llaman `stopNodeCG()`.
|
- **Action:** centralize stop strategy in a single coordinator.
|
||||||
- **Acción:** centralizar estrategia de parada en un solo coordinador.
|
|
||||||
|
|
||||||
### 8.2 Extraer utilidades de proceso SO
|
### 8.2 Platform-specific process utils
|
||||||
|
|
||||||
- **Acción:** separar lógica Windows (`taskkill`) y POSIX (`process.kill`) en módulos específicos.
|
- **Action:** split Windows (`taskkill`) and POSIX (`process.kill`) logic into dedicated modules.
|
||||||
|
|
||||||
### 8.3 Revisar `shell: true` en spawn
|
## 9) Suggested renames
|
||||||
|
|
||||||
- **Motivo:** reducir superficie y comportamiento inesperado.
|
- `waitForNodeCGReady` -> `waitForNodeCGHttpReady`
|
||||||
- **Plan seguro:** introducir feature-flag para comparar comportamiento antes de retirar.
|
- `stopNodeCG` -> `stopNodeCGProcess`
|
||||||
|
- `createLoadingWindow` -> `createSplashWindow`
|
||||||
|
|
||||||
## 9) Renombrados sugeridos (bajo riesgo)
|
> Apply renames in atomic changes with tests to avoid breakages.
|
||||||
|
|
||||||
- `loadingRoute` → `loadingDashboardRoute`.
|
## 10) New files to create
|
||||||
- `dashboardRoute` → `mainDashboardRoute`.
|
|
||||||
- `baseUrl` → `nodecgBaseUrl`.
|
|
||||||
- `launch()` → `launchApplication()`.
|
|
||||||
- `stopNodeCG()` → `stopNodecgProcessGracefully()`.
|
|
||||||
|
|
||||||
> Aplicar renombrados con cambios atómicos y tests para evitar breakages.
|
- `docs/architecture.md` (module map and startup flow).
|
||||||
|
- `docs/troubleshooting.md` (common failures + commands).
|
||||||
|
- `src/main/errors/logger.ts`.
|
||||||
|
- `src/main/main-controller.ts` (orchestrator for startup/shutdown).
|
||||||
|
|
||||||
## 10) Qué crearía nuevo
|
## 11) What to remove (if unused)
|
||||||
|
|
||||||
- `docs/architecture.md` (mapa de módulos y flujo de inicio).
|
- Legacy comments that describe behavior no longer present.
|
||||||
- `docs/troubleshooting.md` (errores comunes).
|
- Duplicate icon-path legacy references if there is already one strategy.
|
||||||
- `.env.example` (variables documentadas y defaults).
|
|
||||||
- `scripts/doctor.mjs` (chequeo preflight).
|
|
||||||
- `tests/main/*.test.ts` (suite base de utilidades y lifecycle).
|
|
||||||
|
|
||||||
## 11) Qué eliminaría (si no se usa)
|
## 12) Phase-based execution plan (without breaking anything)
|
||||||
|
|
||||||
- Configuraciones o scripts redundantes de rebuild nativo cuando no sean necesarios para el bundle actual.
|
1. **Phase 0 – Safety baseline:** add tests for current behavior.
|
||||||
- Referencias legacy de rutas de iconos duplicadas si ya hay estrategia única.
|
2. **Phase 1 – Mechanical refactor:** move code to modules without changing behavior.
|
||||||
|
3. **Phase 2 – Observability:** structured logs and clear errors.
|
||||||
|
4. **Phase 3 – Hardening:** config validation, navigation security, and shutdown.
|
||||||
|
5. **Phase 4 – Build/CI:** icon fixes, pipeline hardening, and smoke tests.
|
||||||
|
6. **Phase 5 – Final cleanup:** renames and residual debt removal.
|
||||||
|
|
||||||
> Eliminar solo tras comprobar uso real en CI y build local.
|
## 13) Per-change acceptance criteria
|
||||||
|
|
||||||
## 12) Plan de ejecución por fases (sin romper nada)
|
- App still starts in dev and packaged mode.
|
||||||
|
- Main dashboard loads after NodeCG readiness.
|
||||||
|
- Controlled shutdown with no orphan NodeCG processes.
|
||||||
|
- Unit and smoke tests pass.
|
||||||
|
|
||||||
1. **Fase 0 – Baseline:** typecheck + build + smoke manual.
|
## 14) Recommended prioritization
|
||||||
2. **Fase 1 – Refactor mecánico:** mover código a módulos sin cambiar comportamiento.
|
|
||||||
3. **Fase 2 – Tests:** cubrir utilidades y lifecycle.
|
|
||||||
4. **Fase 3 – Hardening:** validación de config, seguridad de navegación y shutdown.
|
|
||||||
5. **Fase 4 – Packaging:** iconos por plataforma y CI.
|
|
||||||
6. **Fase 5 – Limpieza final:** renombrados y eliminación de deuda residual.
|
|
||||||
|
|
||||||
## 13) Criterios de aceptación por cambio
|
- **High:** process robustness, config validation, and deterministic shutdown.
|
||||||
|
- **Medium:** observability, docs, and build hardening.
|
||||||
- `npm run typecheck` en verde.
|
- **Low:** cosmetic renames and package-size optimization.
|
||||||
- `npm run build` en verde.
|
|
||||||
- Arranque correcto mostrando loading y luego dashboard.
|
|
||||||
- Cierre correcto sin procesos huérfanos de NodeCG.
|
|
||||||
- Sin cambios visibles no intencionales en UX.
|
|
||||||
|
|
||||||
## 14) Priorización recomendada
|
|
||||||
|
|
||||||
- **Alta:** separación de módulos, tests base, hardening de shutdown y readiness.
|
|
||||||
- **Media:** validación tipada de env vars, logging estructurado, CI.
|
|
||||||
- **Baja:** renombrados cosméticos y optimización de tamaño de paquete.
|
|
||||||
|
|||||||
+16
-16
@@ -1,27 +1,27 @@
|
|||||||
# Troubleshooting
|
# Troubleshooting
|
||||||
|
|
||||||
## `No existe la carpeta NodeCG`
|
## `NodeCG folder does not exist`
|
||||||
|
|
||||||
- Verifica que exista `lib/nodecg`.
|
- Verify `lib/nodecg` exists.
|
||||||
- Asegúrate de que el proyecto contiene una instalación completa de NodeCG.
|
- Make sure the project contains a full NodeCG installation.
|
||||||
|
|
||||||
## `Sin permisos de lectura/escritura sobre NodeCG`
|
## `No read/write permissions on NodeCG`
|
||||||
|
|
||||||
- Ajusta permisos de `lib/nodecg` para el usuario que ejecuta Electron.
|
- Adjust permissions on `lib/nodecg` for the user running Electron.
|
||||||
- En Linux/macOS: `chmod -R u+rw lib/nodecg` (según tu política local).
|
- On Linux/macOS: `chmod -R u+rw lib/nodecg` (according to your local policy).
|
||||||
|
|
||||||
## `El puerto <PORT> ya está en uso`
|
## `Port <PORT> is already in use`
|
||||||
|
|
||||||
- Libera el puerto o define `NODECG_PORT` en `.env`.
|
- Free the port or set `NODECG_PORT` in `.env`.
|
||||||
- Usa `npm run doctor` para validar disponibilidad antes de arrancar.
|
- Use `npm run doctor` to validate availability before startup.
|
||||||
|
|
||||||
## `Timeout esperando NodeCG`
|
## `Timeout while waiting for NodeCG`
|
||||||
|
|
||||||
- Revisa logs de NodeCG en la salida estándar.
|
- Check NodeCG logs in standard output.
|
||||||
- Incrementa `NODECG_STARTUP_TIMEOUT_MS` si el entorno es lento.
|
- Increase `NODECG_STARTUP_TIMEOUT_MS` if the environment is slow.
|
||||||
- Verifica dependencias de NodeCG (`cd lib/nodecg && npm install`).
|
- Verify NodeCG dependencies (`cd lib/nodecg && npm install`).
|
||||||
|
|
||||||
## Build macOS falla por icono
|
## macOS build fails because of icon
|
||||||
|
|
||||||
- La configuración espera `static/icons/icon.icns`.
|
- The configuration expects `static/icons/icon.icns`.
|
||||||
- Crea ese archivo antes de ejecutar empaquetado para macOS.
|
- Create that file before running macOS packaging.
|
||||||
|
|||||||
+9
-9
@@ -16,11 +16,11 @@ function parsePort(name, fallback) {
|
|||||||
const raw = process.env[name] ?? fallback;
|
const raw = process.env[name] ?? fallback;
|
||||||
const parsed = Number.parseInt(raw, 10);
|
const parsed = Number.parseInt(raw, 10);
|
||||||
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) {
|
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) {
|
||||||
addCheck(false, `${name} inválido`, `Debe ser un entero entre 1 y 65535. Valor recibido: '${raw}'.`);
|
addCheck(false, `${name} invalid`, `It must be an integer between 1 and 65535. Received value: '${raw}'.`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
addCheck(true, `${name} válido`, `${parsed}`);
|
addCheck(true, `${name} valid`, `${parsed}`);
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,11 +28,11 @@ function parseIntInRange(name, fallback, min, max) {
|
|||||||
const raw = process.env[name] ?? String(fallback);
|
const raw = process.env[name] ?? String(fallback);
|
||||||
const parsed = Number.parseInt(raw, 10);
|
const parsed = Number.parseInt(raw, 10);
|
||||||
if (!Number.isFinite(parsed) || parsed < min || parsed > max) {
|
if (!Number.isFinite(parsed) || parsed < min || parsed > max) {
|
||||||
addCheck(false, `${name} inválido`, `Debe ser un entero entre ${min} y ${max}. Valor recibido: '${raw}'.`);
|
addCheck(false, `${name} invalid`, `It must be an integer between ${min} and ${max}. Received value: '${raw}'.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
addCheck(true, `${name} válido`, `${parsed}`);
|
addCheck(true, `${name} valid`, `${parsed}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkNodecgInstall() {
|
function checkNodecgInstall() {
|
||||||
@@ -46,9 +46,9 @@ function checkNodecgInstall() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
fs.accessSync(nodecgRootPath, fs.constants.R_OK | fs.constants.W_OK);
|
fs.accessSync(nodecgRootPath, fs.constants.R_OK | fs.constants.W_OK);
|
||||||
addCheck(true, "Permisos lib/nodecg", "Lectura/escritura OK");
|
addCheck(true, "lib/nodecg permissions", "Read/write OK");
|
||||||
} catch {
|
} catch {
|
||||||
addCheck(false, "Permisos lib/nodecg", "Sin permisos de lectura/escritura en lib/nodecg");
|
addCheck(false, "lib/nodecg permissions", "No read/write permissions in lib/nodecg");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,13 +57,13 @@ function checkPortAvailability(port) {
|
|||||||
const server = net.createServer();
|
const server = net.createServer();
|
||||||
|
|
||||||
server.once("error", () => {
|
server.once("error", () => {
|
||||||
addCheck(false, `Puerto ${port}`, "Está ocupado. Libéralo o cambia NODECG_PORT.");
|
addCheck(false, `Port ${port}`, "It is in use. Free it or change NODECG_PORT.");
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
server.listen(port, "127.0.0.1", () => {
|
server.listen(port, "127.0.0.1", () => {
|
||||||
server.close(() => {
|
server.close(() => {
|
||||||
addCheck(true, `Puerto ${port}`, "Disponible");
|
addCheck(true, `Port ${port}`, "Available");
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -92,7 +92,7 @@ async function main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("\nDoctor finalizado: configuración válida.");
|
console.log("\nDoctor finished: valid configuration.");
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export function parseEnvIntInRange(name: string, fallback: number, min: number,
|
|||||||
|
|
||||||
const parsedValue = Number.parseInt(rawValue, 10);
|
const parsedValue = Number.parseInt(rawValue, 10);
|
||||||
if (!Number.isFinite(parsedValue) || parsedValue < min || parsedValue > max) {
|
if (!Number.isFinite(parsedValue) || parsedValue < min || parsedValue > max) {
|
||||||
throw new Error(`La variable ${name} debe ser un entero entre ${min} y ${max}. Valor recibido: '${rawValue}'.`);
|
throw new Error(`The ${name} variable must be an integer between ${min} and ${max}. Received value: '${rawValue}'.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsedValue;
|
return parsedValue;
|
||||||
@@ -68,7 +68,7 @@ export function parseEnvPort(name: string, fallback: string): string {
|
|||||||
|
|
||||||
if (!Number.isFinite(parsedValue) || parsedValue < MIN_TCP_PORT || parsedValue > MAX_TCP_PORT) {
|
if (!Number.isFinite(parsedValue) || parsedValue < MIN_TCP_PORT || parsedValue > MAX_TCP_PORT) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`La variable ${name} debe ser un puerto TCP válido (${MIN_TCP_PORT}-${MAX_TCP_PORT}). Valor recibido: '${rawValue}'.`,
|
`The ${name} variable must be a valid TCP port (${MIN_TCP_PORT}-${MAX_TCP_PORT}). Received value: '${rawValue}'.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -146,9 +146,9 @@ process.on("exit", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
process.on("uncaughtException", (error) => {
|
process.on("uncaughtException", (error) => {
|
||||||
showFatalError("Error inesperado en el proceso principal de Electron.", error);
|
showFatalError("Unexpected error in Electron main process.", error);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on("unhandledRejection", (reason) => {
|
process.on("unhandledRejection", (reason) => {
|
||||||
showFatalError("Promesa no controlada en el proceso principal de Electron.", reason);
|
showFatalError("Unhandled promise in Electron main process.", reason);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export function createNodecgProcessManager({
|
|||||||
const isPortAvailable = await resolvedDeps.probePortAvailable(portAsNumber);
|
const isPortAvailable = await resolvedDeps.probePortAvailable(portAsNumber);
|
||||||
if (!isPortAvailable) {
|
if (!isPortAvailable) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`El puerto ${appConfig.nodecgPort} ya está en uso. Cierra el proceso que lo ocupa o configura NODECG_PORT antes de iniciar.`,
|
`Port ${appConfig.nodecgPort} is already in use. Stop the process using it or set NODECG_PORT before starting.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,16 +110,16 @@ export function createNodecgProcessManager({
|
|||||||
while (Date.now() - startTime < appConfig.startupTimeoutMs) {
|
while (Date.now() - startTime < appConfig.startupTimeoutMs) {
|
||||||
if (!nodecgProcess) {
|
if (!nodecgProcess) {
|
||||||
const exitDetails = lastExit
|
const exitDetails = lastExit
|
||||||
? `Última salida registrada: code=${lastExit.code ?? "null"}, signal=${lastExit.signal ?? "none"}.`
|
? `Last recorded exit: code=${lastExit.code ?? "null"}, signal=${lastExit.signal ?? "none"}.`
|
||||||
: "No se registró código de salida del proceso NodeCG.";
|
: "No NodeCG process exit code was recorded.";
|
||||||
const stderrDetails = lastStderrLine ? `Último stderr: ${lastStderrLine}` : "Sin salida stderr capturada.";
|
const stderrDetails = lastStderrLine ? `Last stderr: ${lastStderrLine}` : "No stderr output captured.";
|
||||||
throw new Error(
|
throw new Error(
|
||||||
[
|
[
|
||||||
"NodeCG terminó antes de estar listo.",
|
"NodeCG exited before becoming ready.",
|
||||||
exitDetails,
|
exitDetails,
|
||||||
stderrDetails,
|
stderrDetails,
|
||||||
`Ruta NodeCG: ${nodecgRootPath}`,
|
`NodeCG path: ${nodecgRootPath}`,
|
||||||
"Revisa que lib/nodecg tenga dependencias instaladas y que el bundle exista.",
|
"Check that lib/nodecg dependencies are installed and the bundle exists.",
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -136,7 +136,7 @@ export function createNodecgProcessManager({
|
|||||||
await sleep(500, resolvedDeps.setTimer);
|
await sleep(500, resolvedDeps.setTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Timeout esperando NodeCG en ${nodecgBaseUrl} (${appConfig.startupTimeoutMs}ms).`);
|
throw new Error(`Timeout waiting for NodeCG at ${nodecgBaseUrl} (${appConfig.startupTimeoutMs}ms).`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const stopNodecgProcessGracefully = (): Promise<void> => {
|
const stopNodecgProcessGracefully = (): Promise<void> => {
|
||||||
@@ -223,23 +223,23 @@ function validateNodecgInstall(
|
|||||||
const bundlePath = path.join(nodecgRootPath, "bundles", bundleName);
|
const bundlePath = path.join(nodecgRootPath, "bundles", bundleName);
|
||||||
|
|
||||||
if (!pathExists(nodecgRootPath)) {
|
if (!pathExists(nodecgRootPath)) {
|
||||||
throw new Error(`No existe la carpeta NodeCG: ${nodecgRootPath}`);
|
throw new Error(`NodeCG folder does not exist: ${nodecgRootPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasReadWriteAccessToPath(nodecgRootPath)) {
|
if (!hasReadWriteAccessToPath(nodecgRootPath)) {
|
||||||
throw new Error(`Sin permisos de lectura/escritura sobre NodeCG: ${nodecgRootPath}`);
|
throw new Error(`No read/write permissions on NodeCG: ${nodecgRootPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pathExists(indexPath)) {
|
if (!pathExists(indexPath)) {
|
||||||
throw new Error(`No se encontró ${indexPath}. Copia una instalación completa de NodeCG en lib/nodecg.`);
|
throw new Error(`${indexPath} was not found. Copy a full NodeCG installation into lib/nodecg.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pathExists(nodecgBootstrapPath)) {
|
if (!pathExists(nodecgBootstrapPath)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
[
|
[
|
||||||
"NodeCG está presente pero faltan dependencias internas.",
|
"NodeCG is present but internal dependencies are missing.",
|
||||||
`No existe: ${nodecgBootstrapPath}`,
|
`Not found: ${nodecgBootstrapPath}`,
|
||||||
"Solución: entra a lib/nodecg e instala dependencias:",
|
"Solution: enter lib/nodecg and install dependencies:",
|
||||||
" npm install",
|
" npm install",
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
);
|
);
|
||||||
@@ -248,9 +248,9 @@ function validateNodecgInstall(
|
|||||||
if (!pathExists(bundlePath)) {
|
if (!pathExists(bundlePath)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
[
|
[
|
||||||
`No se encontró el bundle '${bundleName}'.`,
|
`Bundle '${bundleName}' was not found.`,
|
||||||
`Ruta esperada: ${bundlePath}`,
|
`Expected path: ${bundlePath}`,
|
||||||
"Copia/clona tu bundle dentro de lib/nodecg/bundles antes de ejecutar Electron.",
|
"Copy/clone your bundle inside lib/nodecg/bundles before running Electron.",
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ function getBaseConfig(): AppRuntimeConfig {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test("resolveAppIconPath prioriza iconPathOverride cuando existe", () => {
|
test("resolveAppIconPath prioritizes iconPathOverride when present", () => {
|
||||||
const appConfig: AppRuntimeConfig = {
|
const appConfig: AppRuntimeConfig = {
|
||||||
...getBaseConfig(),
|
...getBaseConfig(),
|
||||||
iconPathOverride: "/custom/icon.ico",
|
iconPathOverride: "/custom/icon.ico",
|
||||||
@@ -30,7 +30,7 @@ test("resolveAppIconPath prioriza iconPathOverride cuando existe", () => {
|
|||||||
assert.equal(iconPath, "/custom/icon.ico");
|
assert.equal(iconPath, "/custom/icon.ico");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("resolveAppIconPath cae al primer icono por defecto existente", () => {
|
test("resolveAppIconPath falls back to the first existing default icon", () => {
|
||||||
const appConfig = getBaseConfig();
|
const appConfig = getBaseConfig();
|
||||||
const expectedIconPath = path.join("/app", "static", "icons", "icon.png");
|
const expectedIconPath = path.join("/app", "static", "icons", "icon.png");
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ test("resolveAppIconPath cae al primer icono por defecto existente", () => {
|
|||||||
assert.equal(iconPath, expectedIconPath);
|
assert.equal(iconPath, expectedIconPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("resolveAppIconPath devuelve undefined cuando no hay iconos", () => {
|
test("resolveAppIconPath returns undefined when no icons exist", () => {
|
||||||
const appConfig = getBaseConfig();
|
const appConfig = getBaseConfig();
|
||||||
|
|
||||||
const iconPath = resolveAppIconPath(appConfig, "/app", () => false);
|
const iconPath = resolveAppIconPath(appConfig, "/app", () => false);
|
||||||
|
|||||||
@@ -5,37 +5,37 @@ import { shouldAllowInternalNavigation, shouldOpenExternalNavigation } from "../
|
|||||||
|
|
||||||
const dashboardUrl = "http://localhost:9090/bundles/scoreko-dev/dashboard/main.html";
|
const dashboardUrl = "http://localhost:9090/bundles/scoreko-dev/dashboard/main.html";
|
||||||
|
|
||||||
test("shouldAllowInternalNavigation permite navegación interna esperada", () => {
|
test("shouldAllowInternalNavigation allows expected internal navigation", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
shouldAllowInternalNavigation("http://127.0.0.1:9090/bundles/scoreko-dev/dashboard/page.html", dashboardUrl),
|
shouldAllowInternalNavigation("http://127.0.0.1:9090/bundles/scoreko-dev/dashboard/page.html", dashboardUrl),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("shouldAllowInternalNavigation rechaza host no permitido", () => {
|
test("shouldAllowInternalNavigation rejects disallowed host", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
shouldAllowInternalNavigation("http://evil.local:9090/bundles/scoreko-dev/dashboard/page.html", dashboardUrl),
|
shouldAllowInternalNavigation("http://evil.local:9090/bundles/scoreko-dev/dashboard/page.html", dashboardUrl),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("shouldAllowInternalNavigation rechaza puerto distinto", () => {
|
test("shouldAllowInternalNavigation rejects different port", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
shouldAllowInternalNavigation("http://localhost:8080/bundles/scoreko-dev/dashboard/page.html", dashboardUrl),
|
shouldAllowInternalNavigation("http://localhost:8080/bundles/scoreko-dev/dashboard/page.html", dashboardUrl),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("shouldAllowInternalNavigation rechaza esquemas inseguros", () => {
|
test("shouldAllowInternalNavigation rejects unsafe schemes", () => {
|
||||||
assert.equal(shouldAllowInternalNavigation("javascript:alert(1)", dashboardUrl), false);
|
assert.equal(shouldAllowInternalNavigation("javascript:alert(1)", dashboardUrl), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("shouldOpenExternalNavigation permite protocolos externos seguros", () => {
|
test("shouldOpenExternalNavigation allows safe external protocols", () => {
|
||||||
assert.equal(shouldOpenExternalNavigation("https://scoreko.com/docs"), true);
|
assert.equal(shouldOpenExternalNavigation("https://scoreko.com/docs"), true);
|
||||||
assert.equal(shouldOpenExternalNavigation("mailto:test@scoreko.com"), true);
|
assert.equal(shouldOpenExternalNavigation("mailto:test@scoreko.com"), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("shouldOpenExternalNavigation rechaza protocolos inseguros", () => {
|
test("shouldOpenExternalNavigation rejects unsafe protocols", () => {
|
||||||
assert.equal(shouldOpenExternalNavigation("file:///etc/passwd"), false);
|
assert.equal(shouldOpenExternalNavigation("file:///etc/passwd"), false);
|
||||||
assert.equal(shouldOpenExternalNavigation("javascript:alert(1)"), false);
|
assert.equal(shouldOpenExternalNavigation("javascript:alert(1)"), false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function getBaseConfig(): AppRuntimeConfig {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test("startNodeCG valida instalación de NodeCG antes de arrancar", async () => {
|
test("startNodeCG validates NodeCG installation before starting", async () => {
|
||||||
const manager = createNodecgProcessManager({
|
const manager = createNodecgProcessManager({
|
||||||
isDev: true,
|
isDev: true,
|
||||||
nodecgRootPath: "/fake/nodecg",
|
nodecgRootPath: "/fake/nodecg",
|
||||||
@@ -41,17 +41,17 @@ test("startNodeCG valida instalación de NodeCG antes de arrancar", async () =>
|
|||||||
deps: {
|
deps: {
|
||||||
pathExists: () => false,
|
pathExists: () => false,
|
||||||
spawnProcess: () => {
|
spawnProcess: () => {
|
||||||
throw new Error("no debe intentar arrancar si la validación falla");
|
throw new Error("it must not try to start if validation fails");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await assert.rejects(async () => {
|
await assert.rejects(async () => {
|
||||||
await manager.startNodecgProcess();
|
await manager.startNodecgProcess();
|
||||||
}, /No existe la carpeta NodeCG/);
|
}, /NodeCG folder does not exist/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("startNodeCG falla si no hay permisos de lectura/escritura", async () => {
|
test("startNodeCG fails when there are no read/write permissions", async () => {
|
||||||
const manager = createNodecgProcessManager({
|
const manager = createNodecgProcessManager({
|
||||||
isDev: true,
|
isDev: true,
|
||||||
nodecgRootPath: "/fake/nodecg",
|
nodecgRootPath: "/fake/nodecg",
|
||||||
@@ -66,10 +66,10 @@ test("startNodeCG falla si no hay permisos de lectura/escritura", async () => {
|
|||||||
|
|
||||||
await assert.rejects(async () => {
|
await assert.rejects(async () => {
|
||||||
await manager.startNodecgProcess();
|
await manager.startNodecgProcess();
|
||||||
}, /Sin permisos de lectura\/escritura/);
|
}, /No read\/write permissions on NodeCG/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("waitForNodeCGReady resuelve cuando el endpoint responde 404", async () => {
|
test("waitForNodeCGReady resolves when endpoint returns 404", async () => {
|
||||||
const child = new MockChildProcess(4321);
|
const child = new MockChildProcess(4321);
|
||||||
const manager = createNodecgProcessManager({
|
const manager = createNodecgProcessManager({
|
||||||
isDev: true,
|
isDev: true,
|
||||||
@@ -99,7 +99,7 @@ test("waitForNodeCGReady resuelve cuando el endpoint responde 404", async () =>
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("stopNodeCG envía SIGTERM y luego SIGKILL si el proceso no sale", async () => {
|
test("stopNodeCG sends SIGTERM and then SIGKILL if the process does not exit", async () => {
|
||||||
const child = new MockChildProcess(9999);
|
const child = new MockChildProcess(9999);
|
||||||
const timers: Array<() => void> = [];
|
const timers: Array<() => void> = [];
|
||||||
const killSignals: Array<{ pid: number; signal: NodeJS.Signals }> = [];
|
const killSignals: Array<{ pid: number; signal: NodeJS.Signals }> = [];
|
||||||
@@ -147,7 +147,7 @@ test("stopNodeCG envía SIGTERM y luego SIGKILL si el proceso no sale", async ()
|
|||||||
await stopPromise;
|
await stopPromise;
|
||||||
});
|
});
|
||||||
|
|
||||||
test("stopNodeCG reutiliza la misma promesa cuando se invoca en paralelo", async () => {
|
test("stopNodeCG reuses the same promise when invoked in parallel", async () => {
|
||||||
const child = new MockChildProcess(5555);
|
const child = new MockChildProcess(5555);
|
||||||
|
|
||||||
const manager = createNodecgProcessManager({
|
const manager = createNodecgProcessManager({
|
||||||
@@ -179,7 +179,7 @@ test("stopNodeCG reutiliza la misma promesa cuando se invoca en paralelo", async
|
|||||||
await firstStop;
|
await firstStop;
|
||||||
});
|
});
|
||||||
|
|
||||||
test("stopNodeCG normaliza timeout negativo a cero", async () => {
|
test("stopNodeCG normalizes negative timeout to zero", async () => {
|
||||||
const child = new MockChildProcess(7777);
|
const child = new MockChildProcess(7777);
|
||||||
const timeouts: number[] = [];
|
const timeouts: number[] = [];
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ test("stopNodeCG normaliza timeout negativo a cero", async () => {
|
|||||||
await stopPromise;
|
await stopPromise;
|
||||||
});
|
});
|
||||||
|
|
||||||
test("startNodeCG falla si el puerto ya está ocupado", async () => {
|
test("startNodeCG fails if the port is already in use", async () => {
|
||||||
const manager = createNodecgProcessManager({
|
const manager = createNodecgProcessManager({
|
||||||
isDev: true,
|
isDev: true,
|
||||||
nodecgRootPath: "/fake/nodecg",
|
nodecgRootPath: "/fake/nodecg",
|
||||||
@@ -234,10 +234,10 @@ test("startNodeCG falla si el puerto ya está ocupado", async () => {
|
|||||||
|
|
||||||
await assert.rejects(async () => {
|
await assert.rejects(async () => {
|
||||||
await manager.startNodecgProcess();
|
await manager.startNodecgProcess();
|
||||||
}, /ya está en uso/);
|
}, /is already in use/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("waitForNodeCGReady expone diagnóstico cuando NodeCG sale antes de readiness", async () => {
|
test("waitForNodeCGReady exposes diagnostics when NodeCG exits before readiness", async () => {
|
||||||
const child = new MockChildProcess(4242);
|
const child = new MockChildProcess(4242);
|
||||||
const manager = createNodecgProcessManager({
|
const manager = createNodecgProcessManager({
|
||||||
isDev: true,
|
isDev: true,
|
||||||
@@ -272,9 +272,9 @@ test("waitForNodeCGReady expone diagnóstico cuando NodeCG sale antes de readine
|
|||||||
},
|
},
|
||||||
(error: unknown) => {
|
(error: unknown) => {
|
||||||
assert.ok(error instanceof Error);
|
assert.ok(error instanceof Error);
|
||||||
assert.match(error.message, /NodeCG terminó antes de estar listo/);
|
assert.match(error.message, /NodeCG exited before becoming ready/);
|
||||||
assert.match(error.message, /Última salida registrada/);
|
assert.match(error.message, /Last recorded exit/);
|
||||||
assert.match(error.message, /Ruta NodeCG: \/fake\/nodecg/);
|
assert.match(error.message, /NodeCG path: \/fake\/nodecg/);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,61 +24,61 @@ function withEnv(name: string, value: string | undefined, run: () => void): void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test("getOptionalEnv devuelve undefined para variable ausente", () => {
|
test("getOptionalEnv returns undefined for missing variable", () => {
|
||||||
withEnv("TEST_OPTIONAL_ENV", undefined, () => {
|
withEnv("TEST_OPTIONAL_ENV", undefined, () => {
|
||||||
assert.equal(getOptionalEnv("TEST_OPTIONAL_ENV"), undefined);
|
assert.equal(getOptionalEnv("TEST_OPTIONAL_ENV"), undefined);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getOptionalEnv recorta espacios y devuelve valor", () => {
|
test("getOptionalEnv trims spaces and returns value", () => {
|
||||||
withEnv("TEST_OPTIONAL_ENV", " scoreko ", () => {
|
withEnv("TEST_OPTIONAL_ENV", " scoreko ", () => {
|
||||||
assert.equal(getOptionalEnv("TEST_OPTIONAL_ENV"), "scoreko");
|
assert.equal(getOptionalEnv("TEST_OPTIONAL_ENV"), "scoreko");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getEnv devuelve fallback para valor vacío", () => {
|
test("getEnv returns fallback for empty value", () => {
|
||||||
withEnv("TEST_ENV", " ", () => {
|
withEnv("TEST_ENV", " ", () => {
|
||||||
assert.equal(getEnv("TEST_ENV", "fallback"), "fallback");
|
assert.equal(getEnv("TEST_ENV", "fallback"), "fallback");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getEnv devuelve el valor cuando existe", () => {
|
test("getEnv returns the value when present", () => {
|
||||||
withEnv("TEST_ENV", "valor", () => {
|
withEnv("TEST_ENV", "value", () => {
|
||||||
assert.equal(getEnv("TEST_ENV", "fallback"), "valor");
|
assert.equal(getEnv("TEST_ENV", "fallback"), "value");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("parseEnvInt devuelve fallback para valores inválidos", () => {
|
test("parseEnvInt returns fallback for invalid values", () => {
|
||||||
withEnv("TEST_ENV_INT", "abc", () => {
|
withEnv("TEST_ENV_INT", "abc", () => {
|
||||||
assert.equal(parseEnvInt("TEST_ENV_INT", 100), 100);
|
assert.equal(parseEnvInt("TEST_ENV_INT", 100), 100);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("parseEnvInt parsea enteros válidos", () => {
|
test("parseEnvInt parses valid integers", () => {
|
||||||
withEnv("TEST_ENV_INT", "4500", () => {
|
withEnv("TEST_ENV_INT", "4500", () => {
|
||||||
assert.equal(parseEnvInt("TEST_ENV_INT", 100), 4500);
|
assert.equal(parseEnvInt("TEST_ENV_INT", 100), 4500);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("parseEnvIntInRange hace hard-fail para valores fuera de rango", () => {
|
test("parseEnvIntInRange hard-fails for out-of-range values", () => {
|
||||||
withEnv("TEST_ENV_INT_RANGE", "999", () => {
|
withEnv("TEST_ENV_INT_RANGE", "999", () => {
|
||||||
assert.throws(() => parseEnvIntInRange("TEST_ENV_INT_RANGE", 100, 0, 100), /debe ser un entero/);
|
assert.throws(() => parseEnvIntInRange("TEST_ENV_INT_RANGE", 100, 0, 100), /must be an integer/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("parseEnvIntInRange acepta valor válido", () => {
|
test("parseEnvIntInRange accepts valid value", () => {
|
||||||
withEnv("TEST_ENV_INT_RANGE", "42", () => {
|
withEnv("TEST_ENV_INT_RANGE", "42", () => {
|
||||||
assert.equal(parseEnvIntInRange("TEST_ENV_INT_RANGE", 100, 0, 100), 42);
|
assert.equal(parseEnvIntInRange("TEST_ENV_INT_RANGE", 100, 0, 100), 42);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("parseEnvPort valida rango TCP", () => {
|
test("parseEnvPort validates TCP range", () => {
|
||||||
withEnv("TEST_ENV_PORT", "70000", () => {
|
withEnv("TEST_ENV_PORT", "70000", () => {
|
||||||
assert.throws(() => parseEnvPort("TEST_ENV_PORT", "9090"), /puerto TCP válido/);
|
assert.throws(() => parseEnvPort("TEST_ENV_PORT", "9090"), /valid TCP port/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("parseEnvPort normaliza el puerto válido", () => {
|
test("parseEnvPort normalizes valid port", () => {
|
||||||
withEnv("TEST_ENV_PORT", "009090", () => {
|
withEnv("TEST_ENV_PORT", "009090", () => {
|
||||||
assert.equal(parseEnvPort("TEST_ENV_PORT", "9090"), "9090");
|
assert.equal(parseEnvPort("TEST_ENV_PORT", "9090"), "9090");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import test from "node:test";
|
|||||||
|
|
||||||
import { getRemainingDelayMs } from "../main/utils/timing";
|
import { getRemainingDelayMs } from "../main/utils/timing";
|
||||||
|
|
||||||
test("getRemainingDelayMs devuelve el tiempo restante cuando aún no se cumple", () => {
|
test("getRemainingDelayMs returns the remaining time when delay has not elapsed", () => {
|
||||||
const remaining = getRemainingDelayMs(10000, 1000, 4000);
|
const remaining = getRemainingDelayMs(10000, 1000, 4000);
|
||||||
assert.equal(remaining, 7000);
|
assert.equal(remaining, 7000);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getRemainingDelayMs devuelve 0 si ya pasó el delay", () => {
|
test("getRemainingDelayMs returns 0 when delay has already elapsed", () => {
|
||||||
const remaining = getRemainingDelayMs(1000, 1000, 5000);
|
const remaining = getRemainingDelayMs(1000, 1000, 5000);
|
||||||
assert.equal(remaining, 0);
|
assert.equal(remaining, 0);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user