feat(main): CC8 — file-backed leveled logger + renderer log sink

Diagnostics were invisible in a packaged build: main catches did a bare
console.error (DevTools suppressed in prod, M31) and the renderer's logError
(M29) wrote to an unreachable console. Add a small leveled logger
(src/main/logger.ts) appending to <userData>/logs/aerofetch.log with size-capped
rotation; all app/fs access is lazy+guarded so it's a safe no-op under tests.

- Route the 5 main-process console.* catch sites (jsonStore write-fail, settings
  write-fail + plaintext-secret warn, cookie loadURL, yt-dlp auto-update)
  through the logger.
- Add a fire-and-forget log:write IPC channel + preload logError() so the
  renderer's logError forwards failures to the main file sink (closes the M29
  "sink" half the code comments deferred to CC8). mockApi + Api type updated.
- Unit-test pure formatLine/composeMessage + no-op safety; update the R6
  jsonStore test to assert against the logger.

The user-facing toast half of CC8 ties to the global status surface (UI25/UX9).
typecheck + 258 tests + lint + prettier green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 08:53:06 -04:00
parent 6bd29efd23
commit 519e522917
13 changed files with 185 additions and 15 deletions
+2 -1
View File
@@ -3,6 +3,7 @@ import { mkdtempSync, rmSync, writeFileSync, readdirSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { readJsonArraySafe, writeJsonAtomic } from '../src/main/jsonStore'
import { logger } from '../src/main/logger'
interface Row {
id: string
@@ -51,7 +52,7 @@ describe('jsonStore atomic write + safe read', () => {
it('reports success/failure instead of swallowing a bad write (R6)', () => {
const d = tmp()
const err = vi.spyOn(console, 'error').mockImplementation(() => {})
const err = vi.spyOn(logger, 'error').mockImplementation(() => {})
try {
// A normal write lands and reports true, with nothing logged.
expect(writeJsonAtomic(join(d, 'data.json'), [{ id: 'a' }])).toBe(true)
+31
View File
@@ -0,0 +1,31 @@
import { describe, it, expect } from 'vitest'
import { formatLine, composeMessage, logger } from '../src/main/logger'
// The logger's file sink needs Electron's `app` (userData path), which is absent in
// the node test env — so here we cover the pure formatting helpers and assert that
// the impure entry points degrade to a safe no-op rather than throwing (CC8).
describe('logger (CC8)', () => {
const now = new Date('2026-07-01T08:30:00.000Z')
it('formatLine renders ISO timestamp + upper-case level + message', () => {
expect(formatLine('error', 'boom', now)).toBe('2026-07-01T08:30:00.000Z [ERROR] boom')
expect(formatLine('info', 'hi', now)).toBe('2026-07-01T08:30:00.000Z [INFO] hi')
})
it('composeMessage passes the label through when there is no detail', () => {
expect(composeMessage('settings write failed')).toBe('settings write failed')
})
it('composeMessage appends an Error message', () => {
expect(composeMessage('write failed', new Error('ENOSPC'))).toBe('write failed: ENOSPC')
})
it('composeMessage stringifies a non-Error detail', () => {
expect(composeMessage('op', 42)).toBe('op: 42')
expect(composeMessage('op', 'bad input')).toBe('op: bad input')
})
it('logger.error is a safe no-op when app/userData is unavailable', () => {
expect(() => logger.error('some op', new Error('x'))).not.toThrow()
})
})