519e522917
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>
80 lines
2.8 KiB
TypeScript
80 lines
2.8 KiB
TypeScript
import { describe, it, expect, afterEach, vi } from 'vitest'
|
|
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
|
|
}
|
|
const isRow = (o: unknown): o is Row =>
|
|
typeof o === 'object' && o !== null && typeof (o as Row).id === 'string'
|
|
|
|
describe('jsonStore atomic write + safe read', () => {
|
|
const dirs: string[] = []
|
|
function tmp(): string {
|
|
const d = mkdtempSync(join(tmpdir(), 'aerofetch-jsonstore-'))
|
|
dirs.push(d)
|
|
return d
|
|
}
|
|
afterEach(() => {
|
|
for (const d of dirs.splice(0)) {
|
|
try {
|
|
rmSync(d, { recursive: true, force: true })
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
})
|
|
|
|
it('round-trips a valid array (R1 write → read)', () => {
|
|
const file = join(tmp(), 'data.json')
|
|
writeJsonAtomic(file, [{ id: 'a' }, { id: 'b' }])
|
|
expect(readJsonArraySafe(file, isRow)).toEqual([{ id: 'a' }, { id: 'b' }])
|
|
})
|
|
|
|
it('leaves no .tmp file behind (atomic temp+rename)', () => {
|
|
const d = tmp()
|
|
writeJsonAtomic(join(d, 'data.json'), [{ id: 'a' }])
|
|
expect(readdirSync(d)).toEqual(['data.json'])
|
|
})
|
|
|
|
it('returns [] for a missing file (first run)', () => {
|
|
expect(readJsonArraySafe(join(tmp(), 'nope.json'), isRow)).toEqual([])
|
|
})
|
|
|
|
it('drops rows that fail validation', () => {
|
|
const file = join(tmp(), 'data.json')
|
|
writeFileSync(file, JSON.stringify([{ id: 'a' }, { nope: 1 }, { id: 'c' }]))
|
|
expect(readJsonArraySafe(file, isRow)).toEqual([{ id: 'a' }, { id: 'c' }])
|
|
})
|
|
|
|
it('reports success/failure instead of swallowing a bad write (R6)', () => {
|
|
const d = tmp()
|
|
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)
|
|
expect(err).not.toHaveBeenCalled()
|
|
// A target under a non-existent directory can't be written — it must report
|
|
// false (so a caller can warn/retry) and log, not throw or silently drop it.
|
|
expect(writeJsonAtomic(join(d, 'no', 'such', 'dir', 'data.json'), [{ id: 'a' }])).toBe(false)
|
|
expect(err).toHaveBeenCalledOnce()
|
|
} finally {
|
|
err.mockRestore()
|
|
}
|
|
// No stray .tmp left behind by the failed write.
|
|
expect(readdirSync(d)).toEqual(['data.json'])
|
|
})
|
|
|
|
it('backs up a corrupt file instead of silently wiping it (R2)', () => {
|
|
const d = tmp()
|
|
const file = join(d, 'data.json')
|
|
writeFileSync(file, '{ truncated, not valid json')
|
|
expect(readJsonArraySafe(file, isRow)).toEqual([])
|
|
const backups = readdirSync(d).filter((f) => f.startsWith('data.json.corrupt-'))
|
|
expect(backups.length).toBe(1)
|
|
})
|
|
})
|