feat: auto-update, тесты и CI/CD
Some checks failed
CI / Typecheck + Tests (push) Has been cancelled
CI / Build (Windows) (push) Has been cancelled

Полная автоматизация релизного цикла.

== Auto-update (electron-updater) ==
- src/main/updater.ts — обёртка над autoUpdater с дискриминированным
  UpdaterStatus union и broadcast через IPC. autoDownload=false,
  пользователь сам жмёт «Скачать». allowDowngrade=false. Проверка
  каждые 6 часов, первая через 5с после старта.
- В dev-режиме (app.isPackaged=false) статус сразу становится
  'unsupported' с пояснением — никаких exceptions из updater'а.
- build.publish в package.json: provider=generic, url указывает на
  Gitea release assets конкретной версии.
- src/main/ipc.ts: 4 новых канала — status/check/download/install.
- src/preload: API window.api.updater* + onUpdaterStatus.
- src/renderer/src/components/UpdaterCard.tsx: HUD-карточка в Settings
  с состояниями idle/checking/available/downloading/downloaded/error,
  прогресс-бар с скоростью в МБ/с.

== Тесты (vitest) ==
- vitest.config.ts с алиасами @shared / @renderer
- 23 теста, все зелёные:
  * format.test.ts — formatCountdown, formatInterval (8 cases)
  * vdf.test.ts — parseVdf / stringifyVdf / round-trip (11 cases)
  * types.test.ts — DEFAULT_SETTINGS, SAMPLE_EXERCISES sanity (4)
- npm scripts: test (watch), test:run (CI)

== CI/CD (Gitea Actions) ==
- .gitea/workflows/ci.yml — на push/PR: typecheck + тесты + smoke-сборка
- .gitea/workflows/release.yml — на тег v*.*.*: сборка NSIS + Gitea release

== Локальный релизный скрипт ==
- scripts/release.ps1 — один скрипт от бампа версии до публикации
  через Gitea API (params: -Bump patch/minor/major, -Version, -DryRun)
- npm run release — обёртка
- RELEASING.md — полная инструкция

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
AnRil
2026-05-16 20:32:59 +07:00
parent 757352e447
commit 92e15e69a3
16 changed files with 1149 additions and 3 deletions

View File

@@ -0,0 +1,56 @@
import { describe, expect, it } from 'vitest'
import { formatCountdown, formatInterval } from './format'
describe('formatCountdown', () => {
it('returns "сейчас" for zero or negative ms', () => {
expect(formatCountdown(0)).toBe('сейчас')
expect(formatCountdown(-1)).toBe('сейчас')
expect(formatCountdown(-100_000)).toBe('сейчас')
})
it('renders sub-minute as seconds only', () => {
expect(formatCountdown(1_000)).toBe('1с')
expect(formatCountdown(45_000)).toBe('45с')
expect(formatCountdown(59_999)).toBe('59с')
})
it('renders minutes with zero-padded seconds', () => {
expect(formatCountdown(60_000)).toBe('1м 00с')
expect(formatCountdown(65_000)).toBe('1м 05с')
expect(formatCountdown(125_000)).toBe('2м 05с')
expect(formatCountdown(599_000)).toBe('9м 59с')
})
it('renders hours with zero-padded minutes and drops seconds', () => {
expect(formatCountdown(3_600_000)).toBe('1ч 00м')
expect(formatCountdown(3_660_000)).toBe('1ч 01м')
expect(formatCountdown(7_245_000)).toBe('2ч 00м')
expect(formatCountdown(7_320_000)).toBe('2ч 02м')
})
it('floors fractional seconds (no rounding up)', () => {
// 999ms > 0 so not "сейчас"; Math.floor(999/1000) = 0 → "0с"
expect(formatCountdown(999)).toBe('0с')
expect(formatCountdown(500)).toBe('0с')
})
})
describe('formatInterval', () => {
it('renders minutes under an hour', () => {
expect(formatInterval(1)).toBe('1 мин')
expect(formatInterval(30)).toBe('30 мин')
expect(formatInterval(59)).toBe('59 мин')
})
it('renders whole hours without minute remainder', () => {
expect(formatInterval(60)).toBe('1 ч')
expect(formatInterval(120)).toBe('2 ч')
expect(formatInterval(180)).toBe('3 ч')
})
it('renders mixed hours+minutes', () => {
expect(formatInterval(61)).toBe('1 ч 1 мин')
expect(formatInterval(90)).toBe('1 ч 30 мин')
expect(formatInterval(125)).toBe('2 ч 5 мин')
})
})