fix(downloads): support offline fallback drafts

- launch Firefox handoffs through authenticated reconnect
- preserve usable drafts when metadata lookup fails
- reject stale metadata results and retry failed rows only
This commit is contained in:
NimBold
2026-06-22 17:21:47 +03:30
parent f469c5d7eb
commit 6b3753949b
10 changed files with 745 additions and 183 deletions
+125
View File
@@ -0,0 +1,125 @@
import { describe, expect, it } from 'vitest';
import {
canSubmitMetadataRows,
mediaFormatSelectorForRow,
metadataSummaryMessage,
reconcileDownloadRows,
refreshFailedMetadataRows,
updateRowIfCurrent,
type AddDownloadDraftRow
} from './addDownloadMetadata';
const row = (
overrides: Partial<AddDownloadDraftRow> = {}
): AddDownloadDraftRow => ({
id: 'row-1',
sourceUrl: 'https://example.com/file.zip',
downloadUrl: 'https://example.com/file.zip',
file: 'file.zip',
status: 'ready',
generation: 1,
isMedia: false,
...overrides
});
describe('add download metadata workflow', () => {
it('preserves rows by normalized source URL and creates only new rows', () => {
const existing = row({ file: 'server-name.zip' });
let nextId = 0;
const rows = reconcileDownloadRows(
'https://example.com/file.zip\nhttps://example.com/new.zip',
[existing],
undefined,
() => `new-${nextId++}`
);
expect(rows[0]).toBe(existing);
expect(rows[1]).toMatchObject({
id: 'new-0',
status: 'loading',
file: 'new.zip'
});
});
it('deduplicates normalized URLs and marks malformed or unsupported URLs invalid', () => {
let nextId = 0;
const rows = reconcileDownloadRows(
'https://example.com/a\nhttps://example.com/a\nfile:///tmp/private\nnot-a-url',
[],
undefined,
() => `row-${nextId++}`
);
expect(rows.map(item => item.status)).toEqual(['loading', 'invalid', 'invalid']);
});
it('refreshes only failed metadata and preserves successful format selection', () => {
const ready = row({
id: 'ready',
isMedia: true,
formats: [{
name: '1080p MP4',
selector: 'best',
ext: 'mp4',
formatLabel: '1080p',
detail: '10 MB',
type: 'Video',
bytes: 10
}],
selectedFormat: 0
});
const failed = row({ id: 'failed', status: 'metadata-error', generation: 4 });
const refreshed = refreshFailedMetadataRows([ready, failed]);
expect(refreshed[0]).toBe(ready);
expect(refreshed[1]).toMatchObject({ status: 'loading', generation: 5 });
});
it('ignores stale metadata results after generation changes', () => {
const current = row({ generation: 2, status: 'loading' });
const updated = updateRowIfCurrent(
[current],
current.id,
current.sourceUrl,
1,
value => ({ ...value, status: 'ready' })
);
expect(updated[0]).toBe(current);
});
it('allows ready and failed rows but blocks loading and invalid rows', () => {
expect(canSubmitMetadataRows([
row(),
row({ id: 'fallback', status: 'metadata-error' })
])).toBe(true);
expect(canSubmitMetadataRows([row({ status: 'loading' })])).toBe(false);
expect(canSubmitMetadataRows([row({ status: 'invalid' })])).toBe(false);
});
it('keeps failed media routing without a format selector', () => {
const failedMedia = row({
status: 'metadata-error',
isMedia: true,
formats: undefined,
selectedFormat: undefined
});
expect(failedMedia.isMedia).toBe(true);
expect(mediaFormatSelectorForRow(failedMedia)).toBeUndefined();
});
it('reports fallback and invalid states accurately', () => {
expect(metadataSummaryMessage([
row(),
row({ id: 'fallback', status: 'metadata-error' })
])).toBe('1 download ready; 1 will use fallback filename and unknown size.');
expect(metadataSummaryMessage([
row({ status: 'metadata-error' })
])).toContain('can still be added');
expect(metadataSummaryMessage([
row({ status: 'invalid' })
])).toContain('Correct or remove 1 invalid URL');
});
});