From 1c912e54758721356c1a6637dc637e7d9837fc07 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 00:38:46 +0000 Subject: [PATCH 1/7] Initial plan From 787878c72d4371d5822f307f3fb5bd954440fcc0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 00:48:14 +0000 Subject: [PATCH 2/7] feat: add electron-log based logging service with IPC support - Install electron-log ^5 dependency - Create app/components/logService.js (renderer-side IPC logging wrapper) - Create __mocks__/electron-log.js (Jest manual mock) - Update main.js: import electron-log, configure transports, add log:send IPC handler - Update app/preload.js: add log:send to allowed IPC channels - Update app/games/registry.js: replace console.warn with electron-log - Update app/interface.js: replace console.error with logger from logService - Create app/components/tests/logService.test.js (14 new tests) - Update app/preload.test.js: add log:send channel coverage - Update app/games/registry.test.js: mock electron-log instead of console.warn - Update app/interface.test.js: assert log:send IPC calls instead of console.error - Update eslint.config.js: enforce no-console in main.js and registry.js - Update .github/copilot-instructions.md: document logging service for future agents Agent-Logs-Url: https://github.com/acrosman/BrainSpeedExercises/sessions/d8111c46-baba-4217-910e-7673656aef6a Co-authored-by: acrosman <2972053+acrosman@users.noreply.github.com> --- .github/copilot-instructions.md | 51 +++++++++- __mocks__/electron-log.js | 31 ++++++ app/components/logService.js | 67 +++++++++++++ app/components/tests/logService.test.js | 124 ++++++++++++++++++++++++ app/games/registry.js | 5 +- app/games/registry.test.js | 32 +++--- app/interface.js | 10 +- app/interface.test.js | 9 +- app/preload.js | 1 + app/preload.test.js | 4 +- eslint.config.js | 18 ++-- main.js | 22 ++++- package-lock.json | 12 ++- package.json | 21 ++-- 14 files changed, 362 insertions(+), 45 deletions(-) create mode 100644 __mocks__/electron-log.js create mode 100644 app/components/logService.js create mode 100644 app/components/tests/logService.test.js diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 24e8c4b..27fd115 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -16,6 +16,7 @@ BrainSpeedExercises is an Electron desktop application that delivers a series of | Linting | [ESLint](https://eslint.org/) (flat config) | Airbnb-style rules; see `.eslint.config.js` | | Accessibility standard | WCAG 2.2 Level AA | Required for all UI | | Progress persistence | Node `fs` (JSON flat-file) via IPC | No external database required | +| Logging | [`electron-log`](https://github.com/megahertz/electron-log) | Main process + IPC bridge for renderer | > **Dependency policy:** All NPM packages are updated regularly. Pin to a major-version range (e.g. `"^38"`) rather than exact versions, and run `npm audit` on every PR. @@ -98,6 +99,7 @@ Valid channels (extend as needed): | `games:load` | renderer → main | Load a specific game by ID | | `progress:save` | renderer → main | Persist player progress | | `progress:load` | renderer → main | Retrieve player progress | +| `log:send` | renderer → main | Forward a log message to `electron-log` | ### 3 — Renderer Process (`app/interface.js`) @@ -210,6 +212,50 @@ game-specific fields that need custom merge logic. --- +### 5c — Logging Service + +All application code **must** use the centralized logging service instead of `console.*` calls. +The `no-console` ESLint rule is enforced; scripts under `scripts/` are exempt. + +#### Main process (`main.js`, `registry.js`, and any other Node-context file) + +Import `electron-log` directly: + +```js +import log from 'electron-log'; +log.warn('Skipping game: manifest missing fields'); +log.error('IPC handler failed', err); +``` + +#### Renderer process (game plugins, `interface.js`, `components/*.js`) + +Import `logger` from the shared Log Service: + +```js +import { logger } from '../../components/logService.js'; +// Adjust the relative path to logService.js as needed. +logger.error('Failed to load game', err); +logger.warn('Unexpected state', { gameId }); +logger.info('Game started', gameId); +``` + +The `logger` object exposes one method per level: `error`, `warn`, `info`, `verbose`, `debug`, +`silly`. Each call fires a `log:send` IPC message to the main process, where it is written through +`electron-log`. The call is fire-and-forget — logging failures never interrupt gameplay. + +**Never** call `window.api.invoke('log:send', ...)` directly. Always use `logger.*`. + +#### Testing + +Mock `electron-log` with `jest.unstable_mockModule('electron-log', ...)` in Node-environment tests +(`/** @jest-environment node */`). A shared manual mock is also available at +`__mocks__/electron-log.js` for automatic mocking. + +For renderer-side tests that exercise code which calls `logger.*`, set `global.window.api.invoke` +to a `jest.fn()` and assert on `'log:send'` calls, or simply verify the call count is correct. + +--- + ### 5b — Shared Game Screen Components All games **must** use the shared CSS classes defined in `app/style.css` for their welcome and end @@ -270,7 +316,8 @@ Rules summary: - Airbnb-style base: `eslint-config-airbnb-base` - Enforce `const`/`let` (no `var`) - Single quotes, trailing commas, semicolons required -- `no-console` warnings in renderer code; allowed in main process +- `no-console` error in all app code; use the logging service instead (see §5c) +- `no-console` is **off** only for scripts under `scripts/` - Max line length: 100 characters - All files under `app/` and root JS files are linted; `node_modules/` is excluded @@ -308,6 +355,8 @@ Use the vscode built-in test runner whenever possible. The Jest config maps Electron's Node modules to lightweight mocks in `__mocks__/electron.js`. Never import Electron APIs directly in renderer-side code — always use the `window.api` bridge. +A matching mock for `electron-log` lives in `__mocks__/electron-log.js`. Tests for main-process modules that import `electron-log` should use `jest.unstable_mockModule('electron-log', ...)` (see `registry.test.js` for an example). + --- ## Accessibility (WCAG 2.2 AA) diff --git a/__mocks__/electron-log.js b/__mocks__/electron-log.js new file mode 100644 index 0000000..eb4b6b5 --- /dev/null +++ b/__mocks__/electron-log.js @@ -0,0 +1,31 @@ +/** + * __mocks__/electron-log.js — Jest manual mock for the electron-log package. + * + * Provides stub implementations of every log-level method so that unit tests + * can spy on logging calls without writing to the filesystem or stdout. + * + * @file Jest mock for electron-log. + */ + +import { jest } from '@jest/globals'; + +const electronLog = { + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + verbose: jest.fn(), + debug: jest.fn(), + silly: jest.fn(), + initialize: jest.fn(), + transports: { + file: { + level: 'info', + resolvePathFn: jest.fn(), + }, + console: { + level: 'warn', + }, + }, +}; + +export default electronLog; diff --git a/app/components/logService.js b/app/components/logService.js new file mode 100644 index 0000000..47a0e34 --- /dev/null +++ b/app/components/logService.js @@ -0,0 +1,67 @@ +/** + * logService.js — Renderer-side logging service for BrainSpeedExercises. + * + * Forwards log messages from the renderer process to the main process via IPC, + * where they are written through the application's electron-log instance. + * + * All renderer code (interface.js, game plugins, components) should import from + * this module instead of calling console.* directly. + * + * Usage: + * import { logger } from '../../components/logService.js'; + * logger.error('Something went wrong', err); + * logger.warn('Unexpected state', { gameId }); + * logger.info('Game started', gameId); + * + * @file Renderer-side IPC logging wrapper. + */ + +/** + * Valid log level strings accepted by the main-process log handler. + * @readonly + * @enum {string} + */ +export const LOG_LEVELS = /** @type {const} */ ( + ['error', 'warn', 'info', 'verbose', 'debug', 'silly'] +); + +/** + * Send a log message to the main process via the `log:send` IPC channel. + * + * Safe to call when `window.api` is unavailable (e.g., in a test environment + * without a real DOM); the call is silently ignored in that case. + * + * @param {string} level - One of the {@link LOG_LEVELS} values. + * @param {...*} args - Message parts; joined with a space before sending. + * @returns {void} + */ +export function log(level, ...args) { + if (typeof window === 'undefined' || !window.api) return; + const message = args + .map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))) + .join(' '); + // Fire-and-forget: swallow any IPC failure so logging never disrupts the caller. + window.api.invoke('log:send', { level, message }).catch(() => {}); +} + +/** + * Structured logger object with one method per log level. + * + * Each method forwards its arguments to {@link log} with the matching level. + * + * @namespace logger + */ +export const logger = { + /** Log an error-level message. @param {...*} args */ + error: (...args) => log('error', ...args), + /** Log a warning-level message. @param {...*} args */ + warn: (...args) => log('warn', ...args), + /** Log an info-level message. @param {...*} args */ + info: (...args) => log('info', ...args), + /** Log a verbose-level message. @param {...*} args */ + verbose: (...args) => log('verbose', ...args), + /** Log a debug-level message. @param {...*} args */ + debug: (...args) => log('debug', ...args), + /** Log a silly-level message. @param {...*} args */ + silly: (...args) => log('silly', ...args), +}; diff --git a/app/components/tests/logService.test.js b/app/components/tests/logService.test.js new file mode 100644 index 0000000..8d063d1 --- /dev/null +++ b/app/components/tests/logService.test.js @@ -0,0 +1,124 @@ +/** @jest-environment node */ +/** + * logService.test.js — Unit tests for the renderer-side logging service. + * + * Exercises log(), logger.*, and the LOG_LEVELS export against a mocked + * window.api IPC bridge. + * + * @file Tests for app/components/logService.js + */ + +import { jest } from '@jest/globals'; + +// ── Module-level mock setup ─────────────────────────────────────────────────── + +const { log, logger, LOG_LEVELS } = await import('../logService.js'); + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +/** + * Build a mock window.api object that records invoke calls. + * @returns {{ mock: jest.Mock }} + */ +function buildApiMock() { + const mock = jest.fn(() => Promise.resolve()); + return { mock }; +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +describe('LOG_LEVELS', () => { + it('exports an array of valid log level strings', () => { + expect(LOG_LEVELS).toEqual( + expect.arrayContaining(['error', 'warn', 'info', 'verbose', 'debug', 'silly']), + ); + }); +}); + +describe('log()', () => { + afterEach(() => { + delete global.window; + }); + + it('does nothing when window is undefined', () => { + // No global.window — should not throw. + expect(() => log('info', 'test message')).not.toThrow(); + }); + + it('does nothing when window.api is absent', () => { + global.window = {}; + expect(() => log('info', 'test message')).not.toThrow(); + }); + + it('calls window.api.invoke with log:send and the correct payload', () => { + const { mock } = buildApiMock(); + global.window = { api: { invoke: mock } }; + + log('error', 'something broke'); + + expect(mock).toHaveBeenCalledWith('log:send', { + level: 'error', + message: 'something broke', + }); + }); + + it('joins multiple arguments into a single space-separated message string', () => { + const { mock } = buildApiMock(); + global.window = { api: { invoke: mock } }; + + log('warn', 'part1', 'part2', 'part3'); + + expect(mock).toHaveBeenCalledWith('log:send', { + level: 'warn', + message: 'part1 part2 part3', + }); + }); + + it('serializes object arguments to JSON', () => { + const { mock } = buildApiMock(); + global.window = { api: { invoke: mock } }; + + log('debug', 'data:', { key: 'value' }); + + expect(mock).toHaveBeenCalledWith('log:send', { + level: 'debug', + message: 'data: {"key":"value"}', + }); + }); + + it('passes the provided level through unchanged', () => { + const { mock } = buildApiMock(); + global.window = { api: { invoke: mock } }; + + log('verbose', 'verbose message'); + + expect(mock).toHaveBeenCalledWith('log:send', { + level: 'verbose', + message: 'verbose message', + }); + }); +}); + +describe('logger', () => { + let mock; + + beforeEach(() => { + ({ mock } = buildApiMock()); + global.window = { api: { invoke: mock } }; + }); + + afterEach(() => { + delete global.window; + }); + + it.each(['error', 'warn', 'info', 'verbose', 'debug', 'silly'])( + 'logger.%s() sends level="%s" to the IPC channel', + (level) => { + logger[level]('test'); + expect(mock).toHaveBeenCalledWith('log:send', { + level, + message: 'test', + }); + }, + ); +}); diff --git a/app/games/registry.js b/app/games/registry.js index 802ae27..e832141 100644 --- a/app/games/registry.js +++ b/app/games/registry.js @@ -10,6 +10,7 @@ import fs from 'fs/promises'; import path from 'path'; +import log from 'electron-log'; const REQUIRED_FIELDS = ['id', 'name', 'description', 'entryPoint']; @@ -36,7 +37,7 @@ export async function scanGamesDirectory(gamesPath) { const manifest = JSON.parse(raw); const missingFields = REQUIRED_FIELDS.filter((f) => !manifest[f]); if (missingFields.length > 0) { - console.warn( + log.warn( `Skipping game "${dir.name}": manifest missing required fields: ${missingFields.join(', ')}`, ); return null; @@ -46,7 +47,7 @@ export async function scanGamesDirectory(gamesPath) { } return manifest; } catch (err) { - console.warn(`Skipping game "${dir.name}": ${err.message}`); + log.warn(`Skipping game "${dir.name}": ${err.message}`); return null; } }), diff --git a/app/games/registry.test.js b/app/games/registry.test.js index 2591a34..fb4ef02 100644 --- a/app/games/registry.test.js +++ b/app/games/registry.test.js @@ -25,10 +25,23 @@ jest.unstable_mockModule('fs/promises', () => ({ }, })); -// In Jest's ESM VM-modules mode, directly assigning a jest.fn() to -// console.warn is more reliable than jest.spyOn across the module boundary. -const mockConsoleWarn = jest.fn(); -const originalConsoleWarn = console.warn; +const mockLogWarn = jest.fn(); + +jest.unstable_mockModule('electron-log', () => ({ + default: { + error: jest.fn(), + warn: mockLogWarn, + info: jest.fn(), + verbose: jest.fn(), + debug: jest.fn(), + silly: jest.fn(), + initialize: jest.fn(), + transports: { + file: { level: 'info', resolvePathFn: jest.fn() }, + console: { level: 'warn' }, + }, + }, +})); const mockPlugin = { name: 'Test Game', @@ -50,13 +63,8 @@ function dirent(name, isDir = true) { beforeEach(() => { jest.resetAllMocks(); - // Re-attach the warn mock after resetAllMocks clears its state. - console.warn = mockConsoleWarn; }); -afterAll(() => { - console.warn = originalConsoleWarn; -}); // ─── scanGamesDirectory ─────────────────────────────────────────────────────── @@ -99,7 +107,7 @@ describe('scanGamesDirectory', () => { const result = await scanGamesDirectory(GAMES_PATH); expect(result).toHaveLength(0); - expect(mockConsoleWarn).toHaveBeenCalledWith(expect.stringContaining('bad-game')); + expect(mockLogWarn).toHaveBeenCalledWith(expect.stringContaining('bad-game')); }); test('skips (with a warning) entries whose manifest.json cannot be read', async () => { @@ -109,7 +117,7 @@ describe('scanGamesDirectory', () => { const result = await scanGamesDirectory(GAMES_PATH); expect(result).toHaveLength(0); - expect(mockConsoleWarn).toHaveBeenCalledWith(expect.stringContaining('broken-game')); + expect(mockLogWarn).toHaveBeenCalledWith(expect.stringContaining('broken-game')); }); test('skips (with a warning) entries with malformed JSON in manifest', async () => { @@ -119,7 +127,7 @@ describe('scanGamesDirectory', () => { const result = await scanGamesDirectory(GAMES_PATH); expect(result).toHaveLength(0); - expect(mockConsoleWarn).toHaveBeenCalled(); + expect(mockLogWarn).toHaveBeenCalled(); }); test('rejects when the games directory does not exist', async () => { diff --git a/app/interface.js b/app/interface.js index b0fbd72..fbeb8c6 100644 --- a/app/interface.js +++ b/app/interface.js @@ -10,6 +10,7 @@ import { createGameCard } from './components/gameCard.js'; import { buildHistoryPanel } from './components/historyView.js'; import { formatDuration, getTodayDateString } from './components/timerService.js'; import { clearHistory } from './components/scoreService.js'; +import { logger } from './components/logService.js'; /** * Inject a game-specific stylesheet into the document
. @@ -105,8 +106,7 @@ async function loadAndInitGame(gameId, gameContainer, announcer) { * @param {Error} [err] - The error that caused the failure. */ function handleGameLoadError(gameId, gameContainer, announcer, err) { - // eslint-disable-next-line no-console - console.error(`Failed to load game "${gameId}".`, err); + logger.error(`Failed to load game "${gameId}".`, err); announcer.textContent = 'Failed to load game. Returning to menu.'; // Return to the game-selection screen so the player is not left on a blank page. window.dispatchEvent(new Event('bsx:return-to-main-menu')); @@ -196,8 +196,7 @@ document.addEventListener('DOMContentLoaded', async () => { try { manifests = await window.api.invoke('games:list'); } catch (err) { - // eslint-disable-next-line no-console - console.error('Failed to load game list:', err); + logger.error('Failed to load game list:', err); gameSelector.textContent = 'Unable to load games. Please restart the app.'; } manifests.forEach((manifest) => { @@ -325,8 +324,7 @@ document.addEventListener('DOMContentLoaded', async () => { viewHistoryBtn.addEventListener('click', historyBtnHandler); } }).catch((err) => { - // eslint-disable-next-line no-console - console.error('Failed to reload progress or game list after returning to menu.', err); + logger.error('Failed to reload progress or game list after returning to menu.', err); updatePlayTimeSummary({}); }); // Re-attach event listener for game selection diff --git a/app/interface.test.js b/app/interface.test.js index 54001f6..5b1c16d 100644 --- a/app/interface.test.js +++ b/app/interface.test.js @@ -157,7 +157,6 @@ describe('interface.js', () => { }); it('shows an error message when games:list rejects', async () => { - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { }); const invoke = jest.fn().mockImplementation((channel) => { if (channel === 'progress:load') return Promise.resolve({}); if (channel === 'games:list') return Promise.reject(new Error('IPC error')); @@ -165,10 +164,12 @@ describe('interface.js', () => { }); global.window.api = { invoke, on: jest.fn() }; await domReadyCallback(); - expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to load game list:', expect.any(Error)); + expect(invoke).toHaveBeenCalledWith('log:send', expect.objectContaining({ + level: 'error', + message: expect.stringContaining('Failed to load game list:'), + })); expect(document.getElementById('game-selector').textContent) .toContain('Unable to load games'); - consoleErrorSpy.mockRestore(); }); it('shows the play-time bar after loading', async () => { @@ -648,7 +649,6 @@ describe('interface.js', () => { describe('bsx:return-to-main-menu error handling', () => { it('handles Promise.all rejection gracefully without leaving UI broken', async () => { - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn()); const invoke = setupApi(); await domReadyCallback(); @@ -663,7 +663,6 @@ describe('interface.js', () => { // The app should not crash; play-time bar should still be in the DOM. expect(document.getElementById('play-time-bar')).not.toBeNull(); - consoleErrorSpy.mockRestore(); }); }); diff --git a/app/preload.js b/app/preload.js index a04fbc3..3518232 100644 --- a/app/preload.js +++ b/app/preload.js @@ -29,6 +29,7 @@ contextBridge.exposeInMainWorld('api', { 'progress:save', 'progress:load', 'progress:reset', + 'log:send', ]; if (validChannels.includes(channel)) { return ipcRenderer.invoke(channel, data); diff --git a/app/preload.test.js b/app/preload.test.js index c50b222..331d400 100644 --- a/app/preload.test.js +++ b/app/preload.test.js @@ -45,7 +45,9 @@ describe('preload.js', () => { }); describe('invoke', () => { - it.each(['games:list', 'games:load', 'progress:save', 'progress:load', 'progress:reset'])( + it.each([ + 'games:list', 'games:load', 'progress:save', 'progress:load', 'progress:reset', 'log:send', + ])( 'calls ipcRenderer.invoke for allowed channel "%s"', async (channel) => { await api.invoke(channel, { data: 1 }); diff --git a/eslint.config.js b/eslint.config.js index f8d6ade..0bf99d4 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -18,9 +18,9 @@ export default [ 'no-console': 'warn', }, }, - // Main process (Node.js) + // Main process (Node.js) — scripts still allowed; main.js uses electron-log { - files: ['main.js', 'forge.config.cjs', 'scripts/**/*.js'], + files: ['forge.config.cjs', 'scripts/**/*.js'], languageOptions: { globals: { ...globals.node, @@ -30,6 +30,15 @@ export default [ 'no-console': 'off', }, }, + // main.js — Node.js globals; console is disallowed (use electron-log) + { + files: ['main.js'], + languageOptions: { + globals: { + ...globals.node, + }, + }, + }, // Renderer process (browser) { files: ['app/interface.js', 'app/components/**/*.js'], @@ -49,7 +58,7 @@ export default [ }, }, }, - // Game registry and progress (Node.js backend) + // Game registry and progress (Node.js backend) — console is disallowed (use electron-log) { files: ['app/games/registry.js', 'app/progress/**/*.js'], languageOptions: { @@ -57,9 +66,6 @@ export default [ ...globals.node, }, }, - rules: { - 'no-console': 'off', - }, }, // Game plugin source files (can use browser globals) { diff --git a/main.js b/main.js index bccdbad..c9f2ce6 100644 --- a/main.js +++ b/main.js @@ -11,6 +11,7 @@ */ import { app, BrowserWindow, ipcMain, session, screen } from 'electron'; import debug from 'electron-debug'; +import log from 'electron-log'; import { readFile, readdir } from 'fs/promises'; import path from 'path'; import { loadProgress, saveProgress, resetProgress } from './app/progress/progressManager.js'; @@ -21,7 +22,11 @@ debug(); // Developer mode flag. const isDev = !app.isPackaged; -debug(); +// Configure electron-log. +// In development, show all levels in the console; in production, only warnings+. +log.transports.file.level = 'info'; +log.transports.console.level = isDev ? 'debug' : 'warn'; +log.initialize(); // Get rid of the deprecated default. app.allowRendererProcessReuse = true; @@ -163,3 +168,18 @@ ipcMain.handle('games:listImages', async (event, { gameId, subfolder }) => { return []; } }); + +/** + * Receive a log message from a renderer process and write it through electron-log. + * + * The renderer sends `{ level, message }` via the `log:send` IPC channel. + * Unrecognised levels fall back to `info`. + * + * @param {Electron.IpcMainInvokeEvent} event + * @param {{ level: string, message: string }} params + */ +ipcMain.handle('log:send', (event, { level, message }) => { + const validLevels = ['error', 'warn', 'info', 'verbose', 'debug', 'silly']; + const fn = validLevels.includes(level) ? level : 'info'; + log[fn](`[renderer] ${message}`); +}); diff --git a/package-lock.json b/package-lock.json index 3c4248e..a45c15b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "electron-debug": "^4" + "electron-debug": "^4", + "electron-log": "^5.4.3" }, "devDependencies": { "@babel/core": "^7", @@ -9605,6 +9606,15 @@ "keyboardevents-areequal": "^0.2.1" } }, + "node_modules/electron-log": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.4.3.tgz", + "integrity": "sha512-sOUsM3LjZdugatazSQ/XTyNcw8dfvH1SYhXWiJyfYodAAKOZdHs0txPiLDXFzOZbhXgAgshQkshH2ccq0feyLQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.321", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", diff --git a/package.json b/package.json index 26d862c..0e11ac6 100644 --- a/package.json +++ b/package.json @@ -27,28 +27,29 @@ "publish": "electron-forge publish" }, "dependencies": { - "electron-debug": "^4" + "electron-debug": "^4", + "electron-log": "^5" }, "overrides": { "yauzl": "^3.2.1" }, "devDependencies": { - "electron": "^39", "@babel/core": "^7", "@babel/preset-env": "^7", + "@electron-forge/cli": "^7", + "@electron-forge/maker-deb": "^7", + "@electron-forge/maker-dmg": "^7", + "@electron-forge/maker-rpm": "^7", + "@electron-forge/maker-squirrel": "^7", + "@electron-forge/publisher-github": "^7", "@jest/globals": "^29", "babel-jest": "^29", + "electron": "^39", "eslint": "^9", + "icon-gen": "^4", "jest": "^30", "jest-environment-jsdom": "^30", - "@electron-forge/cli": "^7", - "@electron-forge/maker-dmg": "^7", - "@electron-forge/maker-squirrel": "^7", - "@electron-forge/maker-deb": "^7", - "@electron-forge/maker-rpm": "^7", - "@electron-forge/publisher-github": "^7", - "sharp": "^0.33", - "icon-gen": "^4" + "sharp": "^0.33" }, "author": "Aaron Crosman", "license": "MIT", From 19d2e4095649bd2ef3fe330094136c28daea51fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 00:49:44 +0000 Subject: [PATCH 3/7] fix: move log.initialize() before transport config, improve comments Agent-Logs-Url: https://github.com/acrosman/BrainSpeedExercises/sessions/d8111c46-baba-4217-910e-7673656aef6a Co-authored-by: acrosman <2972053+acrosman@users.noreply.github.com> --- app/components/logService.js | 3 ++- main.js | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/components/logService.js b/app/components/logService.js index 47a0e34..a914da5 100644 --- a/app/components/logService.js +++ b/app/components/logService.js @@ -40,7 +40,8 @@ export function log(level, ...args) { const message = args .map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))) .join(' '); - // Fire-and-forget: swallow any IPC failure so logging never disrupts the caller. + // Fire-and-forget: if the IPC channel is unavailable (e.g., main process restarting) + // the logging call fails silently so it never disrupts the caller. window.api.invoke('log:send', { level, message }).catch(() => {}); } diff --git a/main.js b/main.js index c9f2ce6..17eb8c2 100644 --- a/main.js +++ b/main.js @@ -22,11 +22,12 @@ debug(); // Developer mode flag. const isDev = !app.isPackaged; -// Configure electron-log. -// In development, show all levels in the console; in production, only warnings+. +// Initialize electron-log, then configure transport levels. +// initialize() must be called first so that default transports are created +// before we override their levels. +log.initialize(); log.transports.file.level = 'info'; log.transports.console.level = isDev ? 'debug' : 'warn'; -log.initialize(); // Get rid of the deprecated default. app.allowRendererProcessReuse = true; From 6fee3ebe4315f3bbdc3f5c1eb44dca2029a145c4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:22:45 +0000 Subject: [PATCH 4/7] fix: remove silly log level, fix integration test, clean up eslint config - Remove 'silly' log level from logService.js, main.js, __mocks__/electron-log.js, registry.test.js, logService.test.js, and copilot-instructions.md - Add moduleNameMapper for electron-log in jest.config.js so the integration test always uses the mock instead of the real electron package - Merge main.js into the Node.js backend section in eslint.config.js - Clean up eslint.config.js comments to reflect purpose rather than changes Agent-Logs-Url: https://github.com/acrosman/BrainSpeedExercises/sessions/a6e448d7-ca97-42b8-8266-a10f337209ef Co-authored-by: acrosman <2972053+acrosman@users.noreply.github.com> --- .github/copilot-instructions.md | 4 ++-- __mocks__/electron-log.js | 1 - app/components/logService.js | 4 +--- app/components/tests/logService.test.js | 5 +++-- app/games/registry.test.js | 1 - eslint.config.js | 15 +++------------ jest.config.js | 3 +++ main.js | 2 +- package-lock.json | 2 +- 9 files changed, 14 insertions(+), 23 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 27fd115..53f869a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -239,8 +239,8 @@ logger.warn('Unexpected state', { gameId }); logger.info('Game started', gameId); ``` -The `logger` object exposes one method per level: `error`, `warn`, `info`, `verbose`, `debug`, -`silly`. Each call fires a `log:send` IPC message to the main process, where it is written through +The `logger` object exposes one method per level: `error`, `warn`, `info`, `verbose`, `debug`. +Each call fires a `log:send` IPC message to the main process, where it is written through `electron-log`. The call is fire-and-forget — logging failures never interrupt gameplay. **Never** call `window.api.invoke('log:send', ...)` directly. Always use `logger.*`. diff --git a/__mocks__/electron-log.js b/__mocks__/electron-log.js index eb4b6b5..00c49d4 100644 --- a/__mocks__/electron-log.js +++ b/__mocks__/electron-log.js @@ -15,7 +15,6 @@ const electronLog = { info: jest.fn(), verbose: jest.fn(), debug: jest.fn(), - silly: jest.fn(), initialize: jest.fn(), transports: { file: { diff --git a/app/components/logService.js b/app/components/logService.js index a914da5..015896c 100644 --- a/app/components/logService.js +++ b/app/components/logService.js @@ -22,7 +22,7 @@ * @enum {string} */ export const LOG_LEVELS = /** @type {const} */ ( - ['error', 'warn', 'info', 'verbose', 'debug', 'silly'] + ['error', 'warn', 'info', 'verbose', 'debug'] ); /** @@ -63,6 +63,4 @@ export const logger = { verbose: (...args) => log('verbose', ...args), /** Log a debug-level message. @param {...*} args */ debug: (...args) => log('debug', ...args), - /** Log a silly-level message. @param {...*} args */ - silly: (...args) => log('silly', ...args), }; diff --git a/app/components/tests/logService.test.js b/app/components/tests/logService.test.js index 8d063d1..d315c82 100644 --- a/app/components/tests/logService.test.js +++ b/app/components/tests/logService.test.js @@ -30,8 +30,9 @@ function buildApiMock() { describe('LOG_LEVELS', () => { it('exports an array of valid log level strings', () => { expect(LOG_LEVELS).toEqual( - expect.arrayContaining(['error', 'warn', 'info', 'verbose', 'debug', 'silly']), + expect.arrayContaining(['error', 'warn', 'info', 'verbose', 'debug']), ); + expect(LOG_LEVELS).not.toContain('silly'); }); }); @@ -111,7 +112,7 @@ describe('logger', () => { delete global.window; }); - it.each(['error', 'warn', 'info', 'verbose', 'debug', 'silly'])( + it.each(['error', 'warn', 'info', 'verbose', 'debug'])( 'logger.%s() sends level="%s" to the IPC channel', (level) => { logger[level]('test'); diff --git a/app/games/registry.test.js b/app/games/registry.test.js index fb4ef02..588004e 100644 --- a/app/games/registry.test.js +++ b/app/games/registry.test.js @@ -34,7 +34,6 @@ jest.unstable_mockModule('electron-log', () => ({ info: jest.fn(), verbose: jest.fn(), debug: jest.fn(), - silly: jest.fn(), initialize: jest.fn(), transports: { file: { level: 'info', resolvePathFn: jest.fn() }, diff --git a/eslint.config.js b/eslint.config.js index 0bf99d4..33a190f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -18,7 +18,7 @@ export default [ 'no-console': 'warn', }, }, - // Main process (Node.js) — scripts still allowed; main.js uses electron-log + // Scripts (Node.js) — console allowed for build tooling { files: ['forge.config.cjs', 'scripts/**/*.js'], languageOptions: { @@ -30,9 +30,9 @@ export default [ 'no-console': 'off', }, }, - // main.js — Node.js globals; console is disallowed (use electron-log) + // Main process and Node.js backend — use electron-log, not console { - files: ['main.js'], + files: ['main.js', 'app/games/registry.js', 'app/progress/**/*.js'], languageOptions: { globals: { ...globals.node, @@ -58,15 +58,6 @@ export default [ }, }, }, - // Game registry and progress (Node.js backend) — console is disallowed (use electron-log) - { - files: ['app/games/registry.js', 'app/progress/**/*.js'], - languageOptions: { - globals: { - ...globals.node, - }, - }, - }, // Game plugin source files (can use browser globals) { files: ['app/games/**/*.js'], diff --git a/jest.config.js b/jest.config.js index b503ada..4e12653 100644 --- a/jest.config.js +++ b/jest.config.js @@ -9,6 +9,9 @@ export default { testEnvironment: 'jsdom', transform: {}, testMatch: ['**/*.test.js'], + moduleNameMapper: { + '^electron-log$': '