feat(logging-p0): add screenshot capture and browser console diagnostics for ERP errors

Enhance ERP automation error diagnostics by capturing PNG screenshots
on every error and forwarding browser console warnings/errors to the
structured logger. Includes automatic cleanup of old screenshots
aligned with the configured log retention period.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-04-04 14:00:48 +08:00
parent c8783a2cef
commit fce8dbc37f
8 changed files with 539 additions and 2 deletions

View File

@@ -3,7 +3,10 @@
* Verifies RequestContext is properly integrated with Logger
*/
import { describe, it, expect } from 'vitest'
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import fs from 'fs'
import path from 'path'
import { getLogDir } from '../../src/main/services/logger/shared'
describe('Logger RequestContext Integration', () => {
it('should export run from request-context', async () => {
@@ -52,3 +55,81 @@ describe('Logger RequestContext Integration', () => {
expect(loggerModule.createLogger).toBeDefined()
})
})
describe('cleanupOldScreenshots', () => {
let unlinkSyncSpy: ReturnType<typeof vi.spyOn>
beforeEach(() => {
vi.clearAllMocks()
})
afterEach(() => {
vi.restoreAllMocks()
})
it('should delete PNG files older than retention period', async () => {
const { cleanupOldScreenshots } = await import('../../src/main/services/logger/shared')
const screenshotDir = path.join(getLogDir(), 'screenshots')
const now = Date.now()
const oldTime = now - 20 * 24 * 60 * 60 * 1000 // 20 days ago
const recentTime = now - 5 * 24 * 60 * 60 * 1000 // 5 days ago
vi.spyOn(fs, 'existsSync').mockImplementation((p) => {
if (typeof p === 'string' && p.includes('screenshots')) return true
return false
})
vi.spyOn(fs, 'readdirSync').mockReturnValue(['err_old.png', 'err_recent.png'] as any)
vi.spyOn(fs, 'statSync').mockImplementation((p) => {
if (typeof p === 'string' && p.includes('err_old')) {
return { mtimeMs: oldTime } as any
}
return { mtimeMs: recentTime } as any
})
unlinkSyncSpy = vi.spyOn(fs, 'unlinkSync').mockReturnValue(undefined)
cleanupOldScreenshots(14)
// Only the old file should be deleted
expect(unlinkSyncSpy).toHaveBeenCalledTimes(1)
expect(unlinkSyncSpy).toHaveBeenCalledWith(path.join(screenshotDir, 'err_old.png'))
})
it('should not delete anything if screenshots directory does not exist', async () => {
const { cleanupOldScreenshots } = await import('../../src/main/services/logger/shared')
vi.spyOn(fs, 'existsSync').mockReturnValue(false)
const readdirSyncSpy = vi.spyOn(fs, 'readdirSync')
cleanupOldScreenshots(14)
expect(readdirSyncSpy).not.toHaveBeenCalled()
})
it('should skip non-PNG files', async () => {
const { cleanupOldScreenshots } = await import('../../src/main/services/logger/shared')
const screenshotDir = path.join(getLogDir(), 'screenshots')
const now = Date.now()
const oldTime = now - 20 * 24 * 60 * 60 * 1000
vi.spyOn(fs, 'existsSync').mockImplementation((p) => {
if (typeof p === 'string' && p.includes('screenshots')) return true
return false
})
vi.spyOn(fs, 'readdirSync').mockReturnValue(['notes.txt', 'data.json', 'err_old.png'] as any)
vi.spyOn(fs, 'statSync').mockReturnValue({ mtimeMs: oldTime } as any)
unlinkSyncSpy = vi.spyOn(fs, 'unlinkSync').mockReturnValue(undefined)
cleanupOldScreenshots(14)
// Only the .png file should be deleted
expect(unlinkSyncSpy).toHaveBeenCalledTimes(1)
expect(unlinkSyncSpy).toHaveBeenCalledWith(path.join(screenshotDir, 'err_old.png'))
})
})