Files
AeroFetch/test/jsonStore.test.ts
T
debont80 7e3e5af52d Fix C1/C2 + reliability cluster (L140/L141/L148, R6/R7)
Critical:
- C1: single source of truth for IPC mock + Settings defaults. DEFAULT_SETTINGS
  in @shared/ipc; main DEFAULTS, renderer FALLBACK, and preview MOCK_SETTINGS all
  derive from it. Whole browser mock collapsed into one typed mockApi.ts; main.tsx
  shrinks to window.api = mockApi. PREVIEW check single-sourced in isPreview.ts.
- C2: break the downloads<->sources circular import via a typed event bus
  (store/coordinator.ts). App eagerly imports sources for side-effects so startup
  load + scheduled --sync + downloadCompleted subscription still run.

Reliability:
- L140/L148: cancel/pause release the active slot synchronously; identity-guarded
  releaseActive; per-spawn cookie jar so a same-id retry/resume is never rejected
  by a stale entry nor run over the concurrency cap.
- L141: renderer history capped at 500 + url de-dup to match main.
- R6: writeJsonAtomic reports/logs write failures and keeps data dirty for retry
  instead of an empty catch.
- R7: encryptSecret warns before the plaintext fallback.

typecheck + 243 tests + eslint green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 20:26:18 -04:00

79 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'
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(console, '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)
})
})