Add configurable download sound and improve cancellation retries
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
test('success sound unlocks on interaction and schedules the supplied chord', async () => {
|
||||
const listeners = {};
|
||||
const oscillators = [];
|
||||
const gains = [];
|
||||
globalThis.document = { addEventListener: (name, callback) => { listeners[name] = callback; } };
|
||||
globalThis.AudioContext = class {
|
||||
state = 'suspended';
|
||||
currentTime = 10;
|
||||
destination = {};
|
||||
async resume() { this.state = 'running'; }
|
||||
createOscillator() {
|
||||
const oscillator = {
|
||||
frequency: { setValueAtTime(value, time) { this.value = value; this.time = time; } }, connect() {}, disconnect() {},
|
||||
start(time) { this.started = time; },
|
||||
stop(time) { this.stopped = time; },
|
||||
};
|
||||
oscillators.push(oscillator);
|
||||
return oscillator;
|
||||
}
|
||||
createGain() {
|
||||
const events = [];
|
||||
gains.push(events);
|
||||
return { gain: {
|
||||
setValueAtTime(value, time) { events.push(['set', value, time]); },
|
||||
exponentialRampToValueAtTime(value, time) { events.push(['exponential', value, time]); },
|
||||
}, connect() {}, disconnect() {} };
|
||||
}
|
||||
};
|
||||
try {
|
||||
const { playDownloadSuccessSound } = await import('../web/utils/download_sound.js');
|
||||
playDownloadSuccessSound();
|
||||
assert.equal(oscillators.length, 0);
|
||||
listeners.pointerdown();
|
||||
playDownloadSuccessSound();
|
||||
assert.deepEqual(oscillators.map(item => item.frequency.value), [523, 659, 784, 1046]);
|
||||
assert.deepEqual(oscillators.map(item => item.started), [10, 10.15, 10.30, 10.47]);
|
||||
oscillators.forEach((item, index) => {
|
||||
const duration = [0.27, 0.33, 0.48, 0.40][index];
|
||||
assert.ok(Math.abs(item.stopped - item.started - duration - 0.02) < 1e-10);
|
||||
assert.deepEqual(gains[index], [
|
||||
['set', 0.0001, item.started],
|
||||
['exponential', [0.055, 0.060, 0.055, 0.022][index], item.started + 0.012],
|
||||
['exponential', 0.0001, item.started + duration],
|
||||
]);
|
||||
item.onended();
|
||||
});
|
||||
const { safeStorage } = await import('../web/resolver/utils/html_utils.js');
|
||||
safeStorage.setItem('ModelResolver.downloadSoundEnabled', 'false');
|
||||
playDownloadSuccessSound();
|
||||
assert.equal(oscillators.length, 4, 'disabled sound must not schedule more notes');
|
||||
} finally {
|
||||
delete globalThis.document;
|
||||
delete globalThis.AudioContext;
|
||||
}
|
||||
});
|
||||
@@ -1,4 +1,78 @@
|
||||
import importlib
|
||||
import threading
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def test_cancel_does_not_wait_for_http_headers_and_closes_late_response():
|
||||
orchestrator = importlib.import_module("core.download.orchestrator")
|
||||
entered = threading.Event()
|
||||
release = threading.Event()
|
||||
closed = threading.Event()
|
||||
cancelled = threading.Event()
|
||||
response = SimpleNamespace(close=closed.set)
|
||||
|
||||
def request(*args, **kwargs):
|
||||
entered.set()
|
||||
assert release.wait(2)
|
||||
return response, "https://example.com/model", {}
|
||||
|
||||
facade = SimpleNamespace(request_public_url=request, cancelled_downloads=set())
|
||||
|
||||
def run():
|
||||
with pytest.raises(orchestrator.DownloadCancelled):
|
||||
orchestrator.request_download_response(facade, "download", "https://example.com/model")
|
||||
cancelled.set()
|
||||
|
||||
worker = threading.Thread(target=run)
|
||||
worker.start()
|
||||
try:
|
||||
assert entered.wait(1)
|
||||
facade.cancelled_downloads.add("download")
|
||||
assert cancelled.wait(1), "Cancellation must finish before HTTP headers arrive"
|
||||
assert not closed.is_set()
|
||||
finally:
|
||||
release.set()
|
||||
worker.join(2)
|
||||
assert closed.wait(1)
|
||||
|
||||
|
||||
def test_download_response_preserves_result_and_network_errors():
|
||||
orchestrator = importlib.import_module("core.download.orchestrator")
|
||||
result = (Mock(), "https://example.com/model", {})
|
||||
facade = SimpleNamespace(request_public_url=Mock(return_value=result), cancelled_downloads=set())
|
||||
assert orchestrator.request_download_response(facade, "download", result[1]) == result
|
||||
facade.request_public_url.side_effect = ValueError("network error")
|
||||
with pytest.raises(ValueError, match="network error"):
|
||||
orchestrator.request_download_response(facade, "download", result[1])
|
||||
|
||||
|
||||
def test_cancel_is_idempotent_and_does_not_revive_terminal_downloads():
|
||||
state_module = importlib.import_module("core.download.state")
|
||||
progress = {"download": {"status": "starting", "speed": 5}}
|
||||
state = SimpleNamespace(
|
||||
download_progress=progress,
|
||||
download_lock=threading.Lock(),
|
||||
cancelled_downloads=set(),
|
||||
aria2_lock=threading.Lock(),
|
||||
aria2_desired_states={},
|
||||
aria2_transfers={},
|
||||
xet_transfers_lock=threading.Lock(),
|
||||
xet_transfers={},
|
||||
)
|
||||
assert state_module.cancel_download("download", state)
|
||||
assert progress["download"] == {"status": "cancelling", "speed": 0}
|
||||
assert "download" in state.cancelled_downloads
|
||||
for status in ("cancelled", "completed", "error"):
|
||||
progress["download"]["status"] = status
|
||||
state.cancelled_downloads.clear()
|
||||
assert state_module.cancel_download("download", state)
|
||||
assert progress["download"]["status"] == status
|
||||
assert not state.cancelled_downloads
|
||||
assert state_module.cancel_download("unknown", state)
|
||||
assert "unknown" not in state.cancelled_downloads
|
||||
|
||||
|
||||
def test_initial_download_progress_preserves_the_shared_contract():
|
||||
|
||||
Reference in New Issue
Block a user