solstone-linux 0.4.3__tar.gz → 0.4.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/CHANGELOG.md +5 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/PKG-INFO +1 -1
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/pyproject.toml +1 -1
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/__init__.py +1 -1
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/sync.py +3 -1
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/upload.py +24 -7
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_sync.py +59 -9
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_upload.py +128 -1
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/.gitignore +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/AGENTS.md +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/CLAUDE.md +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/INSTALL.md +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/LICENSE +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/Makefile +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/README.md +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/contrib/icons/hicolor/128x128/apps/solstone-observer.png +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/contrib/icons/hicolor/16x16/apps/solstone-observer.png +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/contrib/icons/hicolor/24x24/apps/solstone-observer.png +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/contrib/icons/hicolor/256x256/apps/solstone-observer.png +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/contrib/icons/hicolor/32x32/apps/solstone-observer.png +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/contrib/icons/hicolor/48x48/apps/solstone-observer.png +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/contrib/icons/hicolor/512x512/apps/solstone-observer.png +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/contrib/icons/hicolor/64x64/apps/solstone-observer.png +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/contrib/icons/hicolor/scalable/apps/solstone-observer.svg +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/contrib/icons/hicolor/scalable/status/solstone-error.svg +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/contrib/icons/hicolor/scalable/status/solstone-paused.svg +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/contrib/icons/hicolor/scalable/status/solstone-recording.svg +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/contrib/icons/hicolor/scalable/status/solstone-syncing.svg +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/scripts/extract_changelog.sh +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/scripts/release.sh +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/activity.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/audio_detect.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/audio_mute.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/audio_recorder.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/capture_stats.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/chat_bridge.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/cli.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/config.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/dbus_service.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/dbusmenu.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/doctor.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/event_sender.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/icons/hicolor/128x128/apps/solstone-observer.png +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/icons/hicolor/16x16/apps/solstone-observer.png +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/icons/hicolor/24x24/apps/solstone-observer.png +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/icons/hicolor/256x256/apps/solstone-observer.png +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/icons/hicolor/32x32/apps/solstone-observer.png +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/icons/hicolor/48x48/apps/solstone-observer.png +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/icons/hicolor/512x512/apps/solstone-observer.png +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/icons/hicolor/64x64/apps/solstone-observer.png +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/icons/hicolor/scalable/apps/solstone-observer.svg +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/icons/hicolor/scalable/status/solstone-error.svg +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/icons/hicolor/scalable/status/solstone-paused.svg +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/icons/hicolor/scalable/status/solstone-recording.svg +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/icons/hicolor/scalable/status/solstone-syncing.svg +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/install_guard.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/monitor_positions.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/observer.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/recovery.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/screencast.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/session_env.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/sni.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/solstone-linux.service.in +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/streams.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/sync_health.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/src/solstone_linux/tray.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/__init__.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/conftest.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/fixtures/introspection/dbusmenu.xml +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/fixtures/introspection/observer1.xml +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/fixtures/introspection/status_notifier_item.xml +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_activity.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_audio_detect.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_audio_recorder.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_capture_stats.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_chat_bridge.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_cli.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_config.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_dbus_introspection.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_dbus_service.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_dbusmenu.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_docs_mirror.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_doctor.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_event_sender.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_extract_changelog.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_install_guard.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_monitor_positions.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_observer.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_observer_emits_stream_silent_event.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_observer_health_beacon.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_screencast.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_screencast_stop_filters_silent_streams.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_session_env.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_streams.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_sync_health.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_sync_health_surfaces.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_tray.py +0 -0
- {solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_version_match.py +0 -0
|
@@ -4,6 +4,11 @@ All notable changes to solstone-linux are documented here.
|
|
|
4
4
|
The format is based on Keep a Changelog (https://keepachangelog.com/),
|
|
5
5
|
and this project adheres to Semantic Versioning.
|
|
6
6
|
|
|
7
|
+
## [0.4.4] - 2026-07-04
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- stopping or restarting sol now shuts down promptly, even with an upload mid-retry. that upload used to wait out its full retry delay first; now it ends the moment you quit sol or the machine powers off. a bad retry setting in your config no longer leaves the uploader stuck, either.
|
|
11
|
+
|
|
7
12
|
## [0.4.3] - 2026-07-03
|
|
8
13
|
|
|
9
14
|
### Changed
|
|
@@ -8,7 +8,8 @@ background task in the same event loop as capture. Walks cache days
|
|
|
8
8
|
newest-to-oldest, queries server for existing segments, uploads missing ones.
|
|
9
9
|
|
|
10
10
|
Refinements over tmux baseline:
|
|
11
|
-
-
|
|
11
|
+
- Owns long retry/backoff and the circuit breaker; per-upload immediate
|
|
12
|
+
retries are bounded and interruptible in UploadClient
|
|
12
13
|
- Circuit breaker tuned by error type: auth=immediate, transient=5-10
|
|
13
14
|
- Transient circuit breaker recovers via half-open probe with exponential backoff
|
|
14
15
|
- Auth/revoked circuit breaker is permanent (requires restart)
|
|
@@ -527,6 +528,7 @@ class SyncService:
|
|
|
527
528
|
"""Stop the sync service."""
|
|
528
529
|
self._running = False
|
|
529
530
|
self._trigger.set()
|
|
531
|
+
self._client.request_stop()
|
|
530
532
|
|
|
531
533
|
async def run(self) -> None:
|
|
532
534
|
"""Main sync loop — waits for triggers, then syncs."""
|
|
@@ -7,7 +7,8 @@ Extracted from solstone's observe/remote_client.py. Accepts Config
|
|
|
7
7
|
as constructor parameter instead of reading config internally.
|
|
8
8
|
|
|
9
9
|
Refinements over tmux baseline:
|
|
10
|
-
-
|
|
10
|
+
- Bounded immediate in-call retries (MAX_IMMEDIATE_ATTEMPTS); long retry is
|
|
11
|
+
owned by the sync loop + circuit breaker
|
|
11
12
|
- Error classification: auth (401/403) vs transient (5xx/network)
|
|
12
13
|
"""
|
|
13
14
|
|
|
@@ -16,6 +17,7 @@ from __future__ import annotations
|
|
|
16
17
|
import logging
|
|
17
18
|
import platform
|
|
18
19
|
import socket
|
|
20
|
+
import threading
|
|
19
21
|
import time
|
|
20
22
|
from pathlib import Path
|
|
21
23
|
from typing import Any, NamedTuple
|
|
@@ -35,6 +37,11 @@ EVENT_DRAIN_TIMEOUT = 3.0
|
|
|
35
37
|
STREAM_TYPE = "desktop"
|
|
36
38
|
OBSERVER_PROTOCOL_VERSION = 2
|
|
37
39
|
OBSERVER_PROTOCOL_VERSION_HEADER = "X-Solstone-Protocol-Version"
|
|
40
|
+
|
|
41
|
+
# Immediate in-call upload attempts before deferring to the sync loop.
|
|
42
|
+
# Long retry/backoff is owned by SyncService + the circuit breaker, not here.
|
|
43
|
+
MAX_IMMEDIATE_ATTEMPTS = 2
|
|
44
|
+
|
|
38
45
|
_CONTENT_TYPES = {".flac": "audio/flac", ".webm": "video/webm"}
|
|
39
46
|
|
|
40
47
|
|
|
@@ -65,12 +72,16 @@ class UploadClient:
|
|
|
65
72
|
self._key = config.key
|
|
66
73
|
self._stream = config.stream
|
|
67
74
|
self._revoked = False
|
|
75
|
+
self._stop_event = threading.Event()
|
|
68
76
|
self._session = requests.Session()
|
|
69
77
|
self._event_session = requests.Session()
|
|
70
78
|
self._event_sender = EventSender(self.relay_event)
|
|
71
79
|
self._retry_backoff = config.sync_retry_delays or [5, 30, 120, 300]
|
|
72
|
-
#
|
|
73
|
-
|
|
80
|
+
# Immediate in-call attempts: floor at 1, honor a low cap, bound a high one.
|
|
81
|
+
# Long retry is owned by SyncService + circuit breaker (see upload_segment).
|
|
82
|
+
self._immediate_attempts = max(
|
|
83
|
+
1, min(config.sync_max_retries, MAX_IMMEDIATE_ATTEMPTS)
|
|
84
|
+
)
|
|
74
85
|
|
|
75
86
|
@property
|
|
76
87
|
def is_revoked(self) -> bool:
|
|
@@ -80,6 +91,10 @@ class UploadClient:
|
|
|
80
91
|
def is_registered(self) -> bool:
|
|
81
92
|
return bool(self._key)
|
|
82
93
|
|
|
94
|
+
def request_stop(self) -> None:
|
|
95
|
+
"""Signal any in-flight upload retry wait to return promptly (transient)."""
|
|
96
|
+
self._stop_event.set()
|
|
97
|
+
|
|
83
98
|
def _persist_registration(self, config: Config, key: str, stream: str) -> None:
|
|
84
99
|
"""Persist the server-issued handle and locked stream back to config."""
|
|
85
100
|
from .config import save_config
|
|
@@ -171,7 +186,7 @@ class UploadClient:
|
|
|
171
186
|
|
|
172
187
|
url = f"{self._url}/app/observer/ingest"
|
|
173
188
|
|
|
174
|
-
for attempt in range(self.
|
|
189
|
+
for attempt in range(self._immediate_attempts):
|
|
175
190
|
file_handles = []
|
|
176
191
|
files_data = []
|
|
177
192
|
error_type = None
|
|
@@ -243,12 +258,13 @@ class UploadClient:
|
|
|
243
258
|
except Exception:
|
|
244
259
|
pass
|
|
245
260
|
|
|
246
|
-
if attempt < self.
|
|
261
|
+
if attempt < self._immediate_attempts - 1:
|
|
247
262
|
delay = self._retry_backoff[min(attempt, len(self._retry_backoff) - 1)]
|
|
248
|
-
|
|
263
|
+
if self._stop_event.wait(delay):
|
|
264
|
+
return UploadResult(False, error_type=ErrorType.TRANSIENT)
|
|
249
265
|
|
|
250
266
|
logger.error(
|
|
251
|
-
f"Upload failed after {self.
|
|
267
|
+
f"Upload failed after {self._immediate_attempts} attempts: {day}/{segment}"
|
|
252
268
|
)
|
|
253
269
|
return UploadResult(False, error_type=error_type)
|
|
254
270
|
|
|
@@ -328,6 +344,7 @@ class UploadClient:
|
|
|
328
344
|
self._event_sender.start()
|
|
329
345
|
|
|
330
346
|
def stop(self) -> None:
|
|
347
|
+
self._stop_event.set()
|
|
331
348
|
self._event_sender.stop(EVENT_DRAIN_TIMEOUT)
|
|
332
349
|
self._event_session.close()
|
|
333
350
|
self._session.close()
|
|
@@ -34,7 +34,12 @@ from solstone_linux.sync_health import (
|
|
|
34
34
|
load_facts,
|
|
35
35
|
save_facts,
|
|
36
36
|
)
|
|
37
|
-
from solstone_linux.upload import
|
|
37
|
+
from solstone_linux.upload import (
|
|
38
|
+
MAX_IMMEDIATE_ATTEMPTS,
|
|
39
|
+
QueryResult,
|
|
40
|
+
UploadClient,
|
|
41
|
+
UploadResult,
|
|
42
|
+
)
|
|
38
43
|
|
|
39
44
|
|
|
40
45
|
def _sha256(path: Path) -> str:
|
|
@@ -568,20 +573,65 @@ class TestCircuitBreakerRecovery:
|
|
|
568
573
|
|
|
569
574
|
|
|
570
575
|
class TestRetryCapRespected:
|
|
571
|
-
"""Test that upload
|
|
576
|
+
"""Test that upload bounds immediate attempts while honoring low caps."""
|
|
572
577
|
|
|
573
|
-
def
|
|
574
|
-
|
|
575
|
-
|
|
578
|
+
def test_high_config_bounded_to_immediate_cap(self, tmp_path: Path):
|
|
579
|
+
config = Config(
|
|
580
|
+
base_dir=tmp_path,
|
|
581
|
+
server_url="http://localhost:9999",
|
|
582
|
+
key="K",
|
|
583
|
+
)
|
|
576
584
|
config.sync_max_retries = 10
|
|
585
|
+
config.sync_retry_delays = [0]
|
|
577
586
|
client = UploadClient(config)
|
|
578
|
-
|
|
587
|
+
client._session = MagicMock()
|
|
588
|
+
client._session.post.return_value = MagicMock(
|
|
589
|
+
status_code=500,
|
|
590
|
+
text="boom",
|
|
591
|
+
json=lambda: {},
|
|
592
|
+
)
|
|
593
|
+
media = tmp_path / "audio.flac"
|
|
594
|
+
media.write_bytes(b"audio")
|
|
595
|
+
|
|
596
|
+
result = client.upload_segment("20260101", "120000_005", [media])
|
|
597
|
+
|
|
598
|
+
assert client._session.post.call_count == MAX_IMMEDIATE_ATTEMPTS
|
|
599
|
+
assert result.error_type == ErrorType.TRANSIENT
|
|
579
600
|
|
|
580
|
-
def
|
|
581
|
-
config = Config(
|
|
601
|
+
def test_low_config_single_attempt(self, tmp_path: Path):
|
|
602
|
+
config = Config(
|
|
603
|
+
base_dir=tmp_path,
|
|
604
|
+
server_url="http://localhost:9999",
|
|
605
|
+
key="K",
|
|
606
|
+
)
|
|
582
607
|
config.sync_max_retries = 1
|
|
583
608
|
client = UploadClient(config)
|
|
584
|
-
|
|
609
|
+
client._session = MagicMock()
|
|
610
|
+
client._session.post.return_value = MagicMock(
|
|
611
|
+
status_code=500,
|
|
612
|
+
text="boom",
|
|
613
|
+
json=lambda: {},
|
|
614
|
+
)
|
|
615
|
+
media = tmp_path / "audio.flac"
|
|
616
|
+
media.write_bytes(b"audio")
|
|
617
|
+
|
|
618
|
+
result = client.upload_segment("20260101", "120000_005", [media])
|
|
619
|
+
|
|
620
|
+
assert client._session.post.call_count == 1
|
|
621
|
+
assert result.error_type == ErrorType.TRANSIENT
|
|
622
|
+
|
|
623
|
+
def test_sync_stop_signals_client_interrupt(self, tmp_path: Path):
|
|
624
|
+
config = Config(base_dir=tmp_path)
|
|
625
|
+
config.ensure_dirs()
|
|
626
|
+
client = UploadClient(config)
|
|
627
|
+
sync = SyncService(config, client)
|
|
628
|
+
|
|
629
|
+
assert client._stop_event.is_set() is False
|
|
630
|
+
|
|
631
|
+
sync.stop()
|
|
632
|
+
|
|
633
|
+
assert client._stop_event.is_set() is True
|
|
634
|
+
assert sync._running is False
|
|
585
635
|
|
|
586
636
|
|
|
587
637
|
class TestSyncHealthFacts:
|
|
@@ -8,7 +8,11 @@ import pytest
|
|
|
8
8
|
|
|
9
9
|
from solstone_linux.config import Config, load_config
|
|
10
10
|
from solstone_linux.sync_health import ErrorType
|
|
11
|
-
from solstone_linux.upload import
|
|
11
|
+
from solstone_linux.upload import (
|
|
12
|
+
MAX_IMMEDIATE_ATTEMPTS,
|
|
13
|
+
OBSERVER_PROTOCOL_VERSION_HEADER,
|
|
14
|
+
UploadClient,
|
|
15
|
+
)
|
|
12
16
|
|
|
13
17
|
|
|
14
18
|
def test_ensure_registered_posts_descriptor_and_persists(tmp_path: Path):
|
|
@@ -149,6 +153,129 @@ def test_upload_segment_returns_stored_key(
|
|
|
149
153
|
assert result.stored_key == expected_key
|
|
150
154
|
|
|
151
155
|
|
|
156
|
+
def test_upload_bounds_immediate_attempts(tmp_path: Path):
|
|
157
|
+
config = Config(
|
|
158
|
+
base_dir=tmp_path,
|
|
159
|
+
server_url="http://localhost:9999",
|
|
160
|
+
key="K",
|
|
161
|
+
)
|
|
162
|
+
config.sync_max_retries = 10
|
|
163
|
+
config.sync_retry_delays = [0]
|
|
164
|
+
client = UploadClient(config)
|
|
165
|
+
client._session = MagicMock()
|
|
166
|
+
client._session.post.return_value = MagicMock(
|
|
167
|
+
status_code=500,
|
|
168
|
+
text="boom",
|
|
169
|
+
json=lambda: {},
|
|
170
|
+
)
|
|
171
|
+
media = tmp_path / "audio.flac"
|
|
172
|
+
media.write_bytes(b"audio")
|
|
173
|
+
|
|
174
|
+
result = client.upload_segment("20260101", "120000_005", [media])
|
|
175
|
+
|
|
176
|
+
assert client._session.post.call_count == MAX_IMMEDIATE_ATTEMPTS
|
|
177
|
+
assert result.success is False
|
|
178
|
+
assert result.error_type == ErrorType.TRANSIENT
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_upload_low_cap_makes_single_attempt(tmp_path: Path):
|
|
182
|
+
config = Config(
|
|
183
|
+
base_dir=tmp_path,
|
|
184
|
+
server_url="http://localhost:9999",
|
|
185
|
+
key="K",
|
|
186
|
+
)
|
|
187
|
+
config.sync_max_retries = 1
|
|
188
|
+
client = UploadClient(config)
|
|
189
|
+
client._session = MagicMock()
|
|
190
|
+
client._session.post.return_value = MagicMock(
|
|
191
|
+
status_code=500,
|
|
192
|
+
text="boom",
|
|
193
|
+
json=lambda: {},
|
|
194
|
+
)
|
|
195
|
+
media = tmp_path / "audio.flac"
|
|
196
|
+
media.write_bytes(b"audio")
|
|
197
|
+
|
|
198
|
+
result = client.upload_segment("20260101", "120000_005", [media])
|
|
199
|
+
|
|
200
|
+
assert client._session.post.call_count == 1
|
|
201
|
+
assert result.success is False
|
|
202
|
+
assert result.error_type == ErrorType.TRANSIENT
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_upload_zero_retries_makes_single_attempt(tmp_path: Path):
|
|
206
|
+
config = Config(
|
|
207
|
+
base_dir=tmp_path,
|
|
208
|
+
server_url="http://localhost:9999",
|
|
209
|
+
key="K",
|
|
210
|
+
)
|
|
211
|
+
config.sync_max_retries = 0
|
|
212
|
+
client = UploadClient(config)
|
|
213
|
+
client._session = MagicMock()
|
|
214
|
+
client._session.post.return_value = MagicMock(
|
|
215
|
+
status_code=500,
|
|
216
|
+
text="boom",
|
|
217
|
+
json=lambda: {},
|
|
218
|
+
)
|
|
219
|
+
media = tmp_path / "audio.flac"
|
|
220
|
+
media.write_bytes(b"audio")
|
|
221
|
+
|
|
222
|
+
result = client.upload_segment("20260101", "120000_005", [media])
|
|
223
|
+
|
|
224
|
+
assert client._session.post.call_count == 1
|
|
225
|
+
assert result.success is False
|
|
226
|
+
assert result.error_type == ErrorType.TRANSIENT
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_upload_negative_retries_makes_single_attempt(tmp_path: Path):
|
|
230
|
+
config = Config(
|
|
231
|
+
base_dir=tmp_path,
|
|
232
|
+
server_url="http://localhost:9999",
|
|
233
|
+
key="K",
|
|
234
|
+
)
|
|
235
|
+
config.sync_max_retries = -1
|
|
236
|
+
client = UploadClient(config)
|
|
237
|
+
client._session = MagicMock()
|
|
238
|
+
client._session.post.return_value = MagicMock(
|
|
239
|
+
status_code=500,
|
|
240
|
+
text="boom",
|
|
241
|
+
json=lambda: {},
|
|
242
|
+
)
|
|
243
|
+
media = tmp_path / "audio.flac"
|
|
244
|
+
media.write_bytes(b"audio")
|
|
245
|
+
|
|
246
|
+
result = client.upload_segment("20260101", "120000_005", [media])
|
|
247
|
+
|
|
248
|
+
assert client._session.post.call_count == 1
|
|
249
|
+
assert result.success is False
|
|
250
|
+
assert result.error_type == ErrorType.TRANSIENT
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def test_upload_interrupt_during_wait_returns_transient(tmp_path: Path):
|
|
254
|
+
config = Config(
|
|
255
|
+
base_dir=tmp_path,
|
|
256
|
+
server_url="http://localhost:9999",
|
|
257
|
+
key="K",
|
|
258
|
+
)
|
|
259
|
+
config.sync_max_retries = 10
|
|
260
|
+
client = UploadClient(config)
|
|
261
|
+
client._session = MagicMock()
|
|
262
|
+
client._session.post.return_value = MagicMock(
|
|
263
|
+
status_code=500,
|
|
264
|
+
text="boom",
|
|
265
|
+
json=lambda: {},
|
|
266
|
+
)
|
|
267
|
+
media = tmp_path / "audio.flac"
|
|
268
|
+
media.write_bytes(b"audio")
|
|
269
|
+
client.request_stop()
|
|
270
|
+
|
|
271
|
+
result = client.upload_segment("20260101", "120000_005", [media])
|
|
272
|
+
|
|
273
|
+
assert client._session.post.call_count == 1
|
|
274
|
+
assert result.success is False
|
|
275
|
+
assert result.error_type == ErrorType.TRANSIENT
|
|
276
|
+
assert client.is_revoked is False
|
|
277
|
+
|
|
278
|
+
|
|
152
279
|
def test_relay_event_uses_bearer_and_keyless_route(tmp_path: Path):
|
|
153
280
|
config = Config(base_dir=tmp_path, server_url="http://localhost:9999", key="K")
|
|
154
281
|
client = UploadClient(config)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{solstone_linux-0.4.3 → solstone_linux-0.4.4}/contrib/icons/hicolor/16x16/apps/solstone-observer.png
RENAMED
|
File without changes
|
{solstone_linux-0.4.3 → solstone_linux-0.4.4}/contrib/icons/hicolor/24x24/apps/solstone-observer.png
RENAMED
|
File without changes
|
|
File without changes
|
{solstone_linux-0.4.3 → solstone_linux-0.4.4}/contrib/icons/hicolor/32x32/apps/solstone-observer.png
RENAMED
|
File without changes
|
{solstone_linux-0.4.3 → solstone_linux-0.4.4}/contrib/icons/hicolor/48x48/apps/solstone-observer.png
RENAMED
|
File without changes
|
|
File without changes
|
{solstone_linux-0.4.3 → solstone_linux-0.4.4}/contrib/icons/hicolor/64x64/apps/solstone-observer.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/fixtures/introspection/status_notifier_item.xml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_observer_emits_stream_silent_event.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{solstone_linux-0.4.3 → solstone_linux-0.4.4}/tests/test_screencast_stop_filters_silent_streams.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|