mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-09 10:49:29 +00:00
fix(properties): harden progress and torrent diagnostics
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user