fix(properties): harden progress and torrent diagnostics

This commit is contained in:
NimBold
2026-08-07 13:32:52 +03:30
parent e402603edb
commit facb7b3300
16 changed files with 291 additions and 45 deletions
+65 -1
View File
@@ -1,5 +1,10 @@
import { describe, expect, it } from 'vitest';
import { formatDownloadBytes, formatDownloadTotal, resolveDownloadSizeDisplay } from './downloadProgress';
import {
formatDownloadBytes,
formatDownloadTotal,
resolveDownloadFraction,
resolveDownloadSizeDisplay,
} from './downloadProgress';
describe('download progress size display', () => {
it('formats byte counts using the binary units used by the download engines', () => {
@@ -44,3 +49,62 @@ describe('download progress size display', () => {
expect(formatDownloadTotal(display)).toBe('2.40 GB');
});
});
describe('resolveDownloadFraction', () => {
it('reconstructs paused progress from exact persisted byte counters', () => {
expect(resolveDownloadFraction({
status: 'paused',
downloadedBytes: 662 * 1024 ** 2,
totalBytes: 2.94 * 1024 ** 3,
})).toBeCloseTo(0.2199, 3);
});
it('uses a live fraction when it is available', () => {
expect(resolveDownloadFraction({
fraction: 0.37,
downloadedBytes: 90,
totalBytes: 100,
status: 'downloading',
})).toBe(0.37);
});
it('does not infer progress from an estimated media total', () => {
expect(resolveDownloadFraction({
fraction: 0,
downloadedBytes: 900,
totalBytes: 1000,
totalIsEstimate: true,
isMedia: true,
size: '~1000 B',
status: 'paused',
})).toBe(0);
});
it('keeps zero at the start and does not divide by an unknown total', () => {
expect(resolveDownloadFraction({
downloadedBytes: 0,
totalBytes: 0,
status: 'paused',
})).toBe(0);
expect(resolveDownloadFraction({
downloadedBytes: 500,
status: 'paused',
})).toBe(0);
});
it('shows completed downloads as complete even when volatile fraction was removed', () => {
expect(resolveDownloadFraction({
status: 'completed',
downloadedBytes: 0,
totalBytes: 100,
})).toBe(1);
});
it('clamps inconsistent exact byte counters', () => {
expect(resolveDownloadFraction({
downloadedBytes: 150,
totalBytes: 100,
status: 'paused',
})).toBe(1);
});
});
+51
View File
@@ -6,11 +6,62 @@ export interface DownloadSizeDisplay {
fallback: string;
}
export type DownloadFractionInput = {
fraction?: number | null;
downloadedBytes?: number | null;
totalBytes?: number | null;
totalIsEstimate?: boolean | null;
isMedia?: boolean | null;
size?: string | null;
status?: string | null;
};
const BYTE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB'] as const;
const isUsableByteCount = (value: number | null | undefined): value is number =>
typeof value === 'number' && Number.isFinite(value) && value >= 0;
const isUsableFraction = (value: number | null | undefined): value is number =>
typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1;
const clampFraction = (value: number): number => Math.max(0, Math.min(1, value));
/**
* Resolves the fraction shown by progress bars when a live fraction is not
* available. Volatile fractions are intentionally omitted from persisted
* downloads, but paused rows retain exact byte counters so their progress can
* still be reconstructed after restart. Estimated media totals are excluded:
* they describe a provisional denominator and must not become a false visual
* claim about completion.
*/
export const resolveDownloadFraction = ({
fraction,
downloadedBytes,
totalBytes,
totalIsEstimate = false,
isMedia = false,
size,
status
}: DownloadFractionInput): number => {
if (status === 'completed') return 1;
const storedFraction = isUsableFraction(fraction) ? fraction : undefined;
if (storedFraction !== undefined && storedFraction > 0) return storedFraction;
const hasEstimatedTotal = totalIsEstimate === true ||
(isMedia === true && size?.trim().startsWith('~') === true);
if (
!hasEstimatedTotal &&
isUsableByteCount(downloadedBytes) &&
isUsableByteCount(totalBytes) &&
totalBytes > 0
) {
return clampFraction(downloadedBytes / totalBytes);
}
return storedFraction ?? 0;
};
const byteUnitIndex = (bytes: number): number => {
let value = bytes;
let unitIndex = 0;
+16
View File
@@ -1,5 +1,7 @@
import { describe, expect, it } from 'vitest';
import {
formatPropertiesAvailability,
formatPropertiesDiagnosticCount,
getPropertiesAvailabilityDiagnosticState,
getPropertiesPeerDiagnosticState,
} from './propertiesDiagnostics';
@@ -12,6 +14,20 @@ const emptyPeerDiagnostics = {
};
describe('Properties peer diagnostics presentation state', () => {
it('formats swarm availability without exposing floating-point noise', () => {
expect(formatPropertiesAvailability(6.05186170212766, 'en-US')).toBe('6.05');
expect(formatPropertiesAvailability(1.5, 'en-US')).toBe('1.5');
expect(formatPropertiesAvailability(1.5, '')).toBe('1.5');
expect(formatPropertiesAvailability(Number.NaN, 'en-US')).toBe('—');
});
it('formats diagnostic counts with the same locale as availability', () => {
expect(formatPropertiesDiagnosticCount(1234, 'en-US')).toBe('1,234');
expect(formatPropertiesDiagnosticCount(1234, 'fa')).toBe(new Intl.NumberFormat('fa').format(1234));
expect(formatPropertiesDiagnosticCount(-1, 'en-US')).toBe('—');
expect(formatPropertiesDiagnosticCount(Number.MAX_SAFE_INTEGER + 1, 'en-US')).toBe('—');
});
it('keeps a genuine empty response live instead of treating it as unavailable', () => {
expect(getPropertiesPeerDiagnosticState(emptyPeerDiagnostics, false, 'idle')).toBe('live');
});
+11
View File
@@ -1,9 +1,20 @@
import type { TorrentPeerDiagnostics } from '../bindings/TorrentPeerDiagnostics';
import type { TorrentAvailabilitySnapshot } from '../bindings/TorrentAvailabilitySnapshot';
import { resolveAppLocale } from '../i18n/locales';
import type { PropertiesDiagnosticPhase } from '../propertiesBridge';
export type PropertiesDiagnosticValueState = 'live' | 'loading' | 'stale' | 'error' | 'unavailable';
export const formatPropertiesDiagnosticCount = (value: number, locale: string): string => {
if (!Number.isSafeInteger(value) || value < 0) return '—';
return new Intl.NumberFormat(resolveAppLocale(locale)).format(value);
};
export const formatPropertiesAvailability = (availability: number, locale: string): string => {
if (!Number.isFinite(availability) || availability < 0) return '—';
return new Intl.NumberFormat(resolveAppLocale(locale), { maximumFractionDigits: 2 }).format(availability);
};
const getPropertiesDiagnosticValueState = (
hasValue: boolean,
diagnosticsLoading: boolean,