solstone-linux 0.3.3__tar.gz → 0.4.1__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.3.3 → solstone_linux-0.4.1}/AGENTS.md +4 -4
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/CHANGELOG.md +13 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/PKG-INFO +1 -1
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/pyproject.toml +1 -1
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/__init__.py +1 -1
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/cli.py +83 -1
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/config.py +48 -7
- solstone_linux-0.4.1/tests/conftest.py +10 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/tests/test_cli.py +88 -0
- solstone_linux-0.4.1/tests/test_config.py +254 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/tests/test_observer.py +1 -1
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/tests/test_tray.py +1 -0
- solstone_linux-0.3.3/tests/test_config.py +0 -157
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/.gitignore +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/CLAUDE.md +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/INSTALL.md +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/LICENSE +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/Makefile +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/README.md +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/contrib/icons/hicolor/scalable/status/solstone-error.svg +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/contrib/icons/hicolor/scalable/status/solstone-paused.svg +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/contrib/icons/hicolor/scalable/status/solstone-recording.svg +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/contrib/icons/hicolor/scalable/status/solstone-syncing.svg +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/scripts/extract_changelog.sh +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/scripts/release.sh +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/activity.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/audio_detect.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/audio_mute.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/audio_recorder.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/chat_bridge.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/dbus_service.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/dbusmenu.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/doctor.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/icons/hicolor/scalable/status/solstone-error.svg +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/icons/hicolor/scalable/status/solstone-paused.svg +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/icons/hicolor/scalable/status/solstone-recording.svg +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/icons/hicolor/scalable/status/solstone-syncing.svg +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/install_guard.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/monitor_positions.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/observer.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/recovery.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/screencast.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/session_env.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/sni.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/solstone-linux.service.in +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/streams.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/sync.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/sync_health.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/tray.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/src/solstone_linux/upload.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/tests/__init__.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/tests/test_activity.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/tests/test_chat_bridge.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/tests/test_dbus_service.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/tests/test_dbusmenu.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/tests/test_doctor.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/tests/test_extract_changelog.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/tests/test_install_guard.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/tests/test_monitor_positions.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/tests/test_observer_emits_stream_silent_event.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/tests/test_screencast.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/tests/test_screencast_stop_filters_silent_streams.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/tests/test_session_env.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/tests/test_streams.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/tests/test_sync.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/tests/test_sync_health.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/tests/test_sync_health_surfaces.py +0 -0
- {solstone_linux-0.3.3 → solstone_linux-0.4.1}/tests/test_upload.py +0 -0
|
@@ -13,9 +13,9 @@ This is **not** part of the solstone monorepo. It is a standalone package with i
|
|
|
13
13
|
```
|
|
14
14
|
src/solstone_linux/
|
|
15
15
|
__init__.py Package version
|
|
16
|
-
cli.py CLI entry point (run, setup, install-service, status)
|
|
16
|
+
cli.py CLI entry point (run, setup, settings, install-service, status)
|
|
17
17
|
solstone-linux.service.in Systemd unit template (rendered by install-service)
|
|
18
|
-
config.py Config loading/persistence (~/.
|
|
18
|
+
config.py Config loading/persistence (config under ~/.config/solstone-linux/)
|
|
19
19
|
observer.py Main capture loop — state machine (idle/screencast), audio + video
|
|
20
20
|
screencast.py Portal-based multi-monitor recording (xdg-desktop-portal + GStreamer)
|
|
21
21
|
audio_recorder.py Stereo audio recording (mic + system via soundcard)
|
|
@@ -126,10 +126,10 @@ Python packages (in pyproject.toml):
|
|
|
126
126
|
|
|
127
127
|
## Data Paths
|
|
128
128
|
|
|
129
|
-
- Config: `~/.
|
|
129
|
+
- Config: `~/.config/solstone-linux/config.json`
|
|
130
130
|
- Captures: `~/.local/share/solstone-linux/captures/`
|
|
131
131
|
- State: `~/.local/share/solstone-linux/state/`
|
|
132
|
-
- Restore token: `~/.
|
|
132
|
+
- Restore token: `~/.config/solstone-linux/restore_token`
|
|
133
133
|
- Install source marker: `~/.config/solstone-linux/.install-source` (tracks which repo clone owns the pipx install)
|
|
134
134
|
|
|
135
135
|
## Key Patterns
|
|
@@ -4,6 +4,19 @@ 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.1] - 2026-06-17
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- you can now check which version you're running. `solstone-linux --version` prints it, so when you're following along with the release notes or asking for help, you know exactly what you have.
|
|
11
|
+
|
|
12
|
+
## [0.4.0] - 2026-06-17
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- a new `solstone-linux settings` command lets you adjust how this observer behaves after setup, from one place instead of hand-editing a config file. you can change how often it makes a segment, the framerate, whether it starts paused, the chat bridge, and how long it keeps local cache. setup itself stays prompt-free; your identity and pairing are left untouched.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- this observer's settings now live under `~/.config/solstone-linux/`, where linux tools expect config to be. if you're upgrading, the move happens on its own the first time you run, with nothing to redo: no re-setup, no re-pairing. your segments stay exactly where they are.
|
|
19
|
+
|
|
7
20
|
## [0.3.3] - 2026-06-16
|
|
8
21
|
|
|
9
22
|
### Fixed
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
Subcommands:
|
|
7
7
|
run Start capture loop + sync service (default)
|
|
8
8
|
setup Interactive configuration
|
|
9
|
+
settings Edit capture/behavior settings
|
|
9
10
|
install-service Write systemd user unit, enable, start
|
|
10
11
|
status Show capture and sync state
|
|
11
12
|
"""
|
|
@@ -24,7 +25,7 @@ import sys
|
|
|
24
25
|
import time
|
|
25
26
|
from pathlib import Path
|
|
26
27
|
|
|
27
|
-
from . import doctor, streams
|
|
28
|
+
from . import __version__, doctor, streams
|
|
28
29
|
from .config import DEFAULT_SERVER_URL, load_config, save_config
|
|
29
30
|
from .streams import stream_name
|
|
30
31
|
from .sync_health import derive_health, load_facts
|
|
@@ -39,6 +40,63 @@ def _setup_logging(verbose: bool = False) -> None:
|
|
|
39
40
|
)
|
|
40
41
|
|
|
41
42
|
|
|
43
|
+
def _prompt_bool(label: str, current: bool) -> bool:
|
|
44
|
+
while True:
|
|
45
|
+
value = input(f"{label} [{'y' if current else 'n'}]: ").strip().lower()
|
|
46
|
+
if value == "":
|
|
47
|
+
return current
|
|
48
|
+
if value in ("y", "yes"):
|
|
49
|
+
return True
|
|
50
|
+
if value in ("n", "no"):
|
|
51
|
+
return False
|
|
52
|
+
print("Enter y or n.")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _prompt_positive_int(label: str, current: int) -> int:
|
|
56
|
+
while True:
|
|
57
|
+
value = input(f"{label} [{current}]: ").strip()
|
|
58
|
+
if value == "":
|
|
59
|
+
return current
|
|
60
|
+
try:
|
|
61
|
+
parsed = int(value)
|
|
62
|
+
except ValueError:
|
|
63
|
+
print("Enter a positive integer.")
|
|
64
|
+
continue
|
|
65
|
+
if parsed > 0:
|
|
66
|
+
return parsed
|
|
67
|
+
print("Enter a positive integer.")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _prompt_framerate(current: int) -> int:
|
|
71
|
+
while True:
|
|
72
|
+
value = input(f"Capture framerate [{current}]: ").strip()
|
|
73
|
+
if value == "":
|
|
74
|
+
return current
|
|
75
|
+
try:
|
|
76
|
+
parsed = int(value)
|
|
77
|
+
except ValueError:
|
|
78
|
+
print("Enter an integer.")
|
|
79
|
+
continue
|
|
80
|
+
clamped = max(1, min(parsed, 10))
|
|
81
|
+
if clamped != parsed:
|
|
82
|
+
print(f"(clamped to {clamped})")
|
|
83
|
+
return clamped
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _prompt_retention(current: int) -> int:
|
|
87
|
+
while True:
|
|
88
|
+
value = input(
|
|
89
|
+
"Cache retention days (-1 = keep forever, 0 = delete after sync, "
|
|
90
|
+
f"N = keep N days) [{current}]: "
|
|
91
|
+
).strip()
|
|
92
|
+
if value == "":
|
|
93
|
+
return current
|
|
94
|
+
try:
|
|
95
|
+
return int(value)
|
|
96
|
+
except ValueError:
|
|
97
|
+
print("Enter an integer.")
|
|
98
|
+
|
|
99
|
+
|
|
42
100
|
def cmd_run(args: argparse.Namespace) -> int:
|
|
43
101
|
"""Start the capture loop + sync service."""
|
|
44
102
|
from .observer import async_run
|
|
@@ -199,6 +257,23 @@ def _cmd_setup_interactive() -> int:
|
|
|
199
257
|
return 0
|
|
200
258
|
|
|
201
259
|
|
|
260
|
+
def cmd_settings(args: argparse.Namespace) -> int:
|
|
261
|
+
config = load_config()
|
|
262
|
+
config.capture_framerate = _prompt_framerate(config.capture_framerate)
|
|
263
|
+
config.draw_cursor = _prompt_bool("Draw cursor", config.draw_cursor)
|
|
264
|
+
config.start_paused = _prompt_bool("Start paused", config.start_paused)
|
|
265
|
+
config.segment_interval = _prompt_positive_int(
|
|
266
|
+
"Segment interval seconds", config.segment_interval
|
|
267
|
+
)
|
|
268
|
+
config.chat_bridge_enabled = _prompt_bool(
|
|
269
|
+
"Chat bridge enabled", config.chat_bridge_enabled
|
|
270
|
+
)
|
|
271
|
+
config.cache_retention_days = _prompt_retention(config.cache_retention_days)
|
|
272
|
+
save_config(config)
|
|
273
|
+
print(f"\nSettings saved to {config.config_path}")
|
|
274
|
+
return 0
|
|
275
|
+
|
|
276
|
+
|
|
202
277
|
def cmd_doctor(args: argparse.Namespace) -> int:
|
|
203
278
|
return doctor.run_doctor()
|
|
204
279
|
|
|
@@ -406,6 +481,9 @@ def main() -> None:
|
|
|
406
481
|
parser.add_argument(
|
|
407
482
|
"-v", "--verbose", action="store_true", help="Enable debug logging"
|
|
408
483
|
)
|
|
484
|
+
parser.add_argument(
|
|
485
|
+
"--version", action="version", version=f"%(prog)s {__version__}"
|
|
486
|
+
)
|
|
409
487
|
subparsers = parser.add_subparsers(dest="command")
|
|
410
488
|
|
|
411
489
|
# run
|
|
@@ -440,6 +518,9 @@ def main() -> None:
|
|
|
440
518
|
help="Verify install prerequisites",
|
|
441
519
|
)
|
|
442
520
|
|
|
521
|
+
# settings
|
|
522
|
+
subparsers.add_parser("settings", help="Edit capture/behavior settings")
|
|
523
|
+
|
|
443
524
|
# install-service
|
|
444
525
|
subparsers.add_parser("install-service", help="Install systemd user service")
|
|
445
526
|
|
|
@@ -456,6 +537,7 @@ def main() -> None:
|
|
|
456
537
|
"run": cmd_run,
|
|
457
538
|
"setup": cmd_setup,
|
|
458
539
|
"doctor": cmd_doctor,
|
|
540
|
+
"settings": cmd_settings,
|
|
459
541
|
"install-service": cmd_install_service,
|
|
460
542
|
"status": cmd_status,
|
|
461
543
|
}
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
"""Configuration loading and persistence for solstone-linux.
|
|
5
5
|
|
|
6
|
-
Config lives at ~/.
|
|
6
|
+
Config lives at ~/.config/solstone-linux/config.json.
|
|
7
7
|
Captures go to ~/.local/share/solstone-linux/captures/.
|
|
8
|
-
Screencast restore token at ~/.
|
|
8
|
+
Screencast restore token at ~/.config/solstone-linux/restore_token.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
@@ -13,6 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
import json
|
|
14
14
|
import logging
|
|
15
15
|
import os
|
|
16
|
+
import shutil
|
|
16
17
|
import stat
|
|
17
18
|
from dataclasses import dataclass, field
|
|
18
19
|
from pathlib import Path
|
|
@@ -27,6 +28,15 @@ DEFAULT_SYNC_MAX_RETRIES = 10
|
|
|
27
28
|
DEFAULT_SYNC_STALE_THRESHOLD = 600
|
|
28
29
|
|
|
29
30
|
|
|
31
|
+
def _default_config_dir() -> Path:
|
|
32
|
+
xdg = os.environ.get("XDG_CONFIG_HOME")
|
|
33
|
+
if xdg:
|
|
34
|
+
base = Path(xdg)
|
|
35
|
+
if base.is_absolute():
|
|
36
|
+
return base / "solstone-linux"
|
|
37
|
+
return Path.home() / ".config" / "solstone-linux"
|
|
38
|
+
|
|
39
|
+
|
|
30
40
|
@dataclass
|
|
31
41
|
class Config:
|
|
32
42
|
"""Configuration for the Linux desktop observer."""
|
|
@@ -46,15 +56,12 @@ class Config:
|
|
|
46
56
|
draw_cursor: bool = True
|
|
47
57
|
start_paused: bool = False
|
|
48
58
|
base_dir: Path = DEFAULT_BASE_DIR
|
|
59
|
+
config_dir: Path = field(default_factory=_default_config_dir)
|
|
49
60
|
|
|
50
61
|
@property
|
|
51
62
|
def captures_dir(self) -> Path:
|
|
52
63
|
return self.base_dir / "captures"
|
|
53
64
|
|
|
54
|
-
@property
|
|
55
|
-
def config_dir(self) -> Path:
|
|
56
|
-
return self.base_dir / "config"
|
|
57
|
-
|
|
58
65
|
@property
|
|
59
66
|
def state_dir(self) -> Path:
|
|
60
67
|
return self.base_dir / "state"
|
|
@@ -74,11 +81,45 @@ class Config:
|
|
|
74
81
|
self.state_dir.mkdir(parents=True, exist_ok=True)
|
|
75
82
|
|
|
76
83
|
|
|
77
|
-
def
|
|
84
|
+
def _migrate_legacy_config(config: Config) -> None:
|
|
85
|
+
old_dir = config.base_dir / "config"
|
|
86
|
+
if config.config_dir == old_dir:
|
|
87
|
+
return
|
|
88
|
+
if config.config_path.exists():
|
|
89
|
+
return
|
|
90
|
+
old_config = old_dir / "config.json"
|
|
91
|
+
if not old_config.exists():
|
|
92
|
+
return
|
|
93
|
+
try:
|
|
94
|
+
config.config_dir.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
shutil.copy2(old_config, config.config_path)
|
|
96
|
+
os.chmod(config.config_path, stat.S_IRUSR | stat.S_IWUSR)
|
|
97
|
+
old_token = old_dir / "restore_token"
|
|
98
|
+
if old_token.exists():
|
|
99
|
+
shutil.copy2(old_token, config.restore_token_path)
|
|
100
|
+
logger.info(f"Migrated config to {config.config_dir}")
|
|
101
|
+
except OSError as e:
|
|
102
|
+
logger.warning(f"Config migration failed: {e}")
|
|
103
|
+
return
|
|
104
|
+
for p in (old_config, old_dir / "restore_token"):
|
|
105
|
+
try:
|
|
106
|
+
p.unlink()
|
|
107
|
+
except OSError:
|
|
108
|
+
pass
|
|
109
|
+
try:
|
|
110
|
+
old_dir.rmdir()
|
|
111
|
+
except OSError:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def load_config(base_dir: Path | None = None, config_dir: Path | None = None) -> Config:
|
|
78
116
|
"""Load config from disk, returning defaults if not found."""
|
|
79
117
|
config = Config()
|
|
80
118
|
if base_dir:
|
|
81
119
|
config.base_dir = base_dir
|
|
120
|
+
if config_dir:
|
|
121
|
+
config.config_dir = config_dir
|
|
122
|
+
_migrate_legacy_config(config)
|
|
82
123
|
|
|
83
124
|
config_path = config.config_path
|
|
84
125
|
if not config_path.exists():
|
|
@@ -3,15 +3,20 @@
|
|
|
3
3
|
|
|
4
4
|
import argparse
|
|
5
5
|
import os
|
|
6
|
+
import sys
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from unittest.mock import MagicMock
|
|
8
9
|
from unittest.mock import patch
|
|
9
10
|
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from solstone_linux import __version__
|
|
10
14
|
from solstone_linux import cli as cli_module
|
|
11
15
|
from solstone_linux.cli import (
|
|
12
16
|
_cmd_setup_interactive,
|
|
13
17
|
cmd_install_service,
|
|
14
18
|
cmd_setup,
|
|
19
|
+
cmd_settings,
|
|
15
20
|
cmd_status,
|
|
16
21
|
)
|
|
17
22
|
from solstone_linux.config import Config, DEFAULT_SERVER_URL
|
|
@@ -22,6 +27,39 @@ def _args() -> argparse.Namespace:
|
|
|
22
27
|
return argparse.Namespace()
|
|
23
28
|
|
|
24
29
|
|
|
30
|
+
def _settings_config(tmp_path: Path) -> Config:
|
|
31
|
+
return Config(
|
|
32
|
+
base_dir=tmp_path,
|
|
33
|
+
config_dir=tmp_path / "config",
|
|
34
|
+
server_url="https://id",
|
|
35
|
+
key="KKKK",
|
|
36
|
+
stream="strm",
|
|
37
|
+
capture_framerate=2,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _run_settings(tmp_path: Path, inputs: list[str]) -> Config:
|
|
42
|
+
config = _settings_config(tmp_path)
|
|
43
|
+
|
|
44
|
+
with patch("solstone_linux.cli.load_config", return_value=config):
|
|
45
|
+
with patch("solstone_linux.cli.save_config") as save_mock:
|
|
46
|
+
with patch("builtins.input", side_effect=inputs):
|
|
47
|
+
assert cmd_settings(_args()) == 0
|
|
48
|
+
|
|
49
|
+
return save_mock.call_args.args[0]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_main_version_flag(monkeypatch, capsys):
|
|
53
|
+
monkeypatch.setattr(sys, "argv", ["solstone-linux", "--version"])
|
|
54
|
+
|
|
55
|
+
with pytest.raises(SystemExit) as excinfo:
|
|
56
|
+
cli_module.main()
|
|
57
|
+
|
|
58
|
+
assert excinfo.value.code == 0
|
|
59
|
+
out = capsys.readouterr().out
|
|
60
|
+
assert __version__ in out
|
|
61
|
+
|
|
62
|
+
|
|
25
63
|
_BINARY = "/home/user/.local/pipx/venvs/solstone-linux/bin/solstone-linux"
|
|
26
64
|
_EXPECTED_SVGS = {
|
|
27
65
|
"solstone-error.svg",
|
|
@@ -41,6 +79,56 @@ def _is_dir_without_icons(self: Path) -> bool:
|
|
|
41
79
|
return _REAL_IS_DIR(self)
|
|
42
80
|
|
|
43
81
|
|
|
82
|
+
def test_cmd_settings_enter_keeps_all(tmp_path: Path):
|
|
83
|
+
saved_config = _run_settings(tmp_path, ["", "", "", "", "", ""])
|
|
84
|
+
|
|
85
|
+
assert saved_config.capture_framerate == 2
|
|
86
|
+
assert saved_config.draw_cursor is True
|
|
87
|
+
assert saved_config.start_paused is False
|
|
88
|
+
assert saved_config.segment_interval == 300
|
|
89
|
+
assert saved_config.chat_bridge_enabled is True
|
|
90
|
+
assert saved_config.cache_retention_days == 7
|
|
91
|
+
assert saved_config.server_url == "https://id"
|
|
92
|
+
assert saved_config.key == "KKKK"
|
|
93
|
+
assert saved_config.stream == "strm"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_cmd_settings_changes_framerate(tmp_path: Path):
|
|
97
|
+
saved_config = _run_settings(tmp_path, ["5", "", "", "", "", ""])
|
|
98
|
+
|
|
99
|
+
assert saved_config.capture_framerate == 5
|
|
100
|
+
assert saved_config.server_url == "https://id"
|
|
101
|
+
assert saved_config.key == "KKKK"
|
|
102
|
+
assert saved_config.stream == "strm"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_cmd_settings_framerate_clamped(tmp_path: Path):
|
|
106
|
+
saved_config = _run_settings(tmp_path, ["99", "", "", "", "", ""])
|
|
107
|
+
|
|
108
|
+
assert saved_config.capture_framerate == 10
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_cmd_settings_framerate_reprompts_on_invalid(tmp_path: Path):
|
|
112
|
+
saved_config = _run_settings(tmp_path, ["abc", "3", "", "", "", "", ""])
|
|
113
|
+
|
|
114
|
+
assert saved_config.capture_framerate == 3
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_cmd_settings_toggles_bool(tmp_path: Path):
|
|
118
|
+
saved_config = _run_settings(tmp_path, ["", "n", "", "", "", ""])
|
|
119
|
+
|
|
120
|
+
assert saved_config.draw_cursor is False
|
|
121
|
+
assert saved_config.server_url == "https://id"
|
|
122
|
+
assert saved_config.key == "KKKK"
|
|
123
|
+
assert saved_config.stream == "strm"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_cmd_settings_retention_semantics(tmp_path: Path):
|
|
127
|
+
saved_config = _run_settings(tmp_path, ["", "", "", "", "", "-1"])
|
|
128
|
+
|
|
129
|
+
assert saved_config.cache_retention_days == -1
|
|
130
|
+
|
|
131
|
+
|
|
44
132
|
def test_cmd_status_prints_sync_health(tmp_path: Path, monkeypatch, capsys):
|
|
45
133
|
config = Config(
|
|
46
134
|
base_dir=tmp_path,
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (c) 2026 sol pbc
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import stat
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from solstone_linux.config import Config, load_config, save_config
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestConfig:
|
|
14
|
+
def test_defaults(self):
|
|
15
|
+
config = Config()
|
|
16
|
+
assert config.server_url == ""
|
|
17
|
+
assert config.key == ""
|
|
18
|
+
assert config.segment_interval == 300
|
|
19
|
+
|
|
20
|
+
def test_captures_dir(self):
|
|
21
|
+
config = Config()
|
|
22
|
+
assert config.captures_dir == config.base_dir / "captures"
|
|
23
|
+
|
|
24
|
+
def test_restore_token_path(self):
|
|
25
|
+
config = Config()
|
|
26
|
+
assert config.restore_token_path == config.config_dir / "restore_token"
|
|
27
|
+
|
|
28
|
+
def test_config_dir_uses_absolute_xdg(self, tmp_path: Path, monkeypatch):
|
|
29
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
30
|
+
config = Config()
|
|
31
|
+
|
|
32
|
+
assert config.config_dir == tmp_path / "solstone-linux"
|
|
33
|
+
assert config.config_path == tmp_path / "solstone-linux" / "config.json"
|
|
34
|
+
assert (
|
|
35
|
+
config.restore_token_path == tmp_path / "solstone-linux" / "restore_token"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def test_config_dir_ignores_relative_xdg(self, monkeypatch):
|
|
39
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", "relative/path")
|
|
40
|
+
|
|
41
|
+
assert Config().config_dir == Path.home() / ".config" / "solstone-linux"
|
|
42
|
+
|
|
43
|
+
def test_config_dir_falls_back_when_xdg_unset(self, monkeypatch):
|
|
44
|
+
monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
|
|
45
|
+
|
|
46
|
+
assert Config().config_dir == Path.home() / ".config" / "solstone-linux"
|
|
47
|
+
|
|
48
|
+
def test_round_trip(self, tmp_path: Path):
|
|
49
|
+
config = Config(base_dir=tmp_path, config_dir=tmp_path / "config")
|
|
50
|
+
config.server_url = "https://example.com"
|
|
51
|
+
config.key = "test-key-123"
|
|
52
|
+
config.stream = "archon"
|
|
53
|
+
config.segment_interval = 600
|
|
54
|
+
|
|
55
|
+
save_config(config)
|
|
56
|
+
|
|
57
|
+
loaded = load_config(base_dir=tmp_path, config_dir=tmp_path / "config")
|
|
58
|
+
assert loaded.server_url == "https://example.com"
|
|
59
|
+
assert loaded.key == "test-key-123"
|
|
60
|
+
assert loaded.stream == "archon"
|
|
61
|
+
assert loaded.segment_interval == 600
|
|
62
|
+
|
|
63
|
+
def test_load_missing(self, tmp_path: Path):
|
|
64
|
+
config = load_config(base_dir=tmp_path, config_dir=tmp_path / "config")
|
|
65
|
+
assert config.server_url == ""
|
|
66
|
+
assert config.key == ""
|
|
67
|
+
|
|
68
|
+
def test_load_corrupt(self, tmp_path: Path):
|
|
69
|
+
config_dir = tmp_path / "config"
|
|
70
|
+
config_dir.mkdir(parents=True)
|
|
71
|
+
(config_dir / "config.json").write_text("not json!")
|
|
72
|
+
|
|
73
|
+
config = load_config(base_dir=tmp_path, config_dir=tmp_path / "config")
|
|
74
|
+
assert config.server_url == ""
|
|
75
|
+
|
|
76
|
+
def test_permissions(self, tmp_path: Path):
|
|
77
|
+
config = Config(base_dir=tmp_path, config_dir=tmp_path / "config")
|
|
78
|
+
config.server_url = "https://example.com"
|
|
79
|
+
config.key = "secret"
|
|
80
|
+
save_config(config)
|
|
81
|
+
|
|
82
|
+
mode = config.config_path.stat().st_mode & 0o777
|
|
83
|
+
assert mode == 0o600
|
|
84
|
+
|
|
85
|
+
def test_sync_config_roundtrip(self, tmp_path: Path):
|
|
86
|
+
config = Config(base_dir=tmp_path, config_dir=tmp_path / "config")
|
|
87
|
+
config.sync_retry_delays = [10, 60, 300]
|
|
88
|
+
config.sync_max_retries = 5
|
|
89
|
+
save_config(config)
|
|
90
|
+
|
|
91
|
+
loaded = load_config(base_dir=tmp_path, config_dir=tmp_path / "config")
|
|
92
|
+
assert loaded.sync_retry_delays == [10, 60, 300]
|
|
93
|
+
assert loaded.sync_max_retries == 5
|
|
94
|
+
|
|
95
|
+
def test_cache_retention_days_roundtrip(self, tmp_path: Path):
|
|
96
|
+
config = Config(base_dir=tmp_path, config_dir=tmp_path / "config")
|
|
97
|
+
config.cache_retention_days = 14
|
|
98
|
+
save_config(config)
|
|
99
|
+
|
|
100
|
+
loaded = load_config(base_dir=tmp_path, config_dir=tmp_path / "config")
|
|
101
|
+
assert loaded.cache_retention_days == 14
|
|
102
|
+
|
|
103
|
+
def test_cache_retention_days_default(self, tmp_path: Path):
|
|
104
|
+
"""Existing configs without cache_retention_days default to 7."""
|
|
105
|
+
config_dir = tmp_path / "config"
|
|
106
|
+
config_dir.mkdir(parents=True)
|
|
107
|
+
(config_dir / "config.json").write_text('{"server_url": "http://test"}')
|
|
108
|
+
|
|
109
|
+
loaded = load_config(base_dir=tmp_path, config_dir=tmp_path / "config")
|
|
110
|
+
assert loaded.cache_retention_days == 7
|
|
111
|
+
|
|
112
|
+
def test_capture_framerate_default(self):
|
|
113
|
+
config = Config()
|
|
114
|
+
assert config.capture_framerate == 1
|
|
115
|
+
|
|
116
|
+
def test_draw_cursor_default(self):
|
|
117
|
+
config = Config()
|
|
118
|
+
assert config.draw_cursor is True
|
|
119
|
+
|
|
120
|
+
def test_capture_framerate_roundtrip(self, tmp_path: Path):
|
|
121
|
+
config = Config(base_dir=tmp_path, config_dir=tmp_path / "config")
|
|
122
|
+
config.capture_framerate = 2
|
|
123
|
+
save_config(config)
|
|
124
|
+
|
|
125
|
+
loaded = load_config(base_dir=tmp_path, config_dir=tmp_path / "config")
|
|
126
|
+
assert loaded.capture_framerate == 2
|
|
127
|
+
|
|
128
|
+
def test_draw_cursor_roundtrip(self, tmp_path: Path):
|
|
129
|
+
config = Config(base_dir=tmp_path, config_dir=tmp_path / "config")
|
|
130
|
+
config.draw_cursor = False
|
|
131
|
+
save_config(config)
|
|
132
|
+
|
|
133
|
+
loaded = load_config(base_dir=tmp_path, config_dir=tmp_path / "config")
|
|
134
|
+
assert loaded.draw_cursor is False
|
|
135
|
+
|
|
136
|
+
def test_capture_framerate_defaults_on_old_config(self, tmp_path: Path):
|
|
137
|
+
"""Existing configs without capture_framerate default to 1."""
|
|
138
|
+
config_dir = tmp_path / "config"
|
|
139
|
+
config_dir.mkdir(parents=True)
|
|
140
|
+
(config_dir / "config.json").write_text('{"server_url": "http://test"}')
|
|
141
|
+
|
|
142
|
+
loaded = load_config(base_dir=tmp_path, config_dir=tmp_path / "config")
|
|
143
|
+
assert loaded.capture_framerate == 1
|
|
144
|
+
assert loaded.draw_cursor is True
|
|
145
|
+
|
|
146
|
+
def test_capture_framerate_clamped_to_max(self, tmp_path: Path):
|
|
147
|
+
config_dir = tmp_path / "config"
|
|
148
|
+
config_dir.mkdir(parents=True)
|
|
149
|
+
(config_dir / "config.json").write_text('{"capture_framerate": 999}')
|
|
150
|
+
|
|
151
|
+
loaded = load_config(base_dir=tmp_path, config_dir=tmp_path / "config")
|
|
152
|
+
assert loaded.capture_framerate == 10
|
|
153
|
+
|
|
154
|
+
def test_capture_framerate_clamped_to_min(self, tmp_path: Path):
|
|
155
|
+
config_dir = tmp_path / "config"
|
|
156
|
+
config_dir.mkdir(parents=True)
|
|
157
|
+
(config_dir / "config.json").write_text('{"capture_framerate": 0}')
|
|
158
|
+
|
|
159
|
+
loaded = load_config(base_dir=tmp_path, config_dir=tmp_path / "config")
|
|
160
|
+
assert loaded.capture_framerate == 1
|
|
161
|
+
|
|
162
|
+
def test_start_paused_default(self):
|
|
163
|
+
config = Config()
|
|
164
|
+
assert config.start_paused is False
|
|
165
|
+
|
|
166
|
+
def test_start_paused_roundtrip(self, tmp_path: Path):
|
|
167
|
+
config = Config(base_dir=tmp_path, config_dir=tmp_path / "config")
|
|
168
|
+
config.start_paused = True
|
|
169
|
+
save_config(config)
|
|
170
|
+
|
|
171
|
+
loaded = load_config(base_dir=tmp_path, config_dir=tmp_path / "config")
|
|
172
|
+
assert loaded.start_paused is True
|
|
173
|
+
|
|
174
|
+
def test_start_paused_defaults_on_old_config(self, tmp_path: Path):
|
|
175
|
+
"""Existing configs without start_paused default to False."""
|
|
176
|
+
config_dir = tmp_path / "config"
|
|
177
|
+
config_dir.mkdir(parents=True)
|
|
178
|
+
(config_dir / "config.json").write_text('{"server_url": "http://test"}')
|
|
179
|
+
|
|
180
|
+
loaded = load_config(base_dir=tmp_path, config_dir=tmp_path / "config")
|
|
181
|
+
assert loaded.start_paused is False
|
|
182
|
+
|
|
183
|
+
def test_migrates_legacy_config(self, tmp_path: Path, caplog):
|
|
184
|
+
old_dir = tmp_path / "config"
|
|
185
|
+
old_dir.mkdir()
|
|
186
|
+
old_config = old_dir / "config.json"
|
|
187
|
+
old_config.write_text(
|
|
188
|
+
json.dumps(
|
|
189
|
+
{
|
|
190
|
+
"server_url": "https://example.com",
|
|
191
|
+
"key": "test-key-123",
|
|
192
|
+
"stream": "archon",
|
|
193
|
+
"capture_framerate": 3,
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
os.chmod(old_config, stat.S_IRUSR | stat.S_IWUSR)
|
|
198
|
+
old_token = old_dir / "restore_token"
|
|
199
|
+
old_token.write_text("tok")
|
|
200
|
+
new_dir = tmp_path / "newcfg"
|
|
201
|
+
|
|
202
|
+
with caplog.at_level(logging.INFO):
|
|
203
|
+
loaded = load_config(base_dir=tmp_path, config_dir=new_dir)
|
|
204
|
+
|
|
205
|
+
new_config = new_dir / "config.json"
|
|
206
|
+
new_token = new_dir / "restore_token"
|
|
207
|
+
assert loaded.server_url == "https://example.com"
|
|
208
|
+
assert loaded.key == "test-key-123"
|
|
209
|
+
assert loaded.stream == "archon"
|
|
210
|
+
assert loaded.capture_framerate == 3
|
|
211
|
+
assert new_config.exists()
|
|
212
|
+
assert stat.S_IMODE(new_config.stat().st_mode) == 0o600
|
|
213
|
+
assert new_token.read_text() == "tok"
|
|
214
|
+
assert not old_config.exists()
|
|
215
|
+
assert not old_token.exists()
|
|
216
|
+
assert not old_dir.exists()
|
|
217
|
+
migration_records = [
|
|
218
|
+
record for record in caplog.records if "Migrated config" in record.message
|
|
219
|
+
]
|
|
220
|
+
assert len(migration_records) == 1
|
|
221
|
+
|
|
222
|
+
snapshot = (
|
|
223
|
+
new_config.read_text(),
|
|
224
|
+
new_token.read_text(),
|
|
225
|
+
stat.S_IMODE(new_config.stat().st_mode),
|
|
226
|
+
)
|
|
227
|
+
caplog.clear()
|
|
228
|
+
|
|
229
|
+
with caplog.at_level(logging.INFO):
|
|
230
|
+
loaded_again = load_config(base_dir=tmp_path, config_dir=new_dir)
|
|
231
|
+
|
|
232
|
+
assert loaded_again.capture_framerate == 3
|
|
233
|
+
assert [
|
|
234
|
+
record for record in caplog.records if "Migrated config" in record.message
|
|
235
|
+
] == []
|
|
236
|
+
assert (
|
|
237
|
+
new_config.read_text(),
|
|
238
|
+
new_token.read_text(),
|
|
239
|
+
stat.S_IMODE(new_config.stat().st_mode),
|
|
240
|
+
) == snapshot
|
|
241
|
+
|
|
242
|
+
def test_no_migration_when_config_dir_is_legacy(self, tmp_path: Path):
|
|
243
|
+
config_dir = tmp_path / "config"
|
|
244
|
+
config_dir.mkdir()
|
|
245
|
+
config_path = config_dir / "config.json"
|
|
246
|
+
content = '{"server_url": "http://test", "capture_framerate": 4}'
|
|
247
|
+
config_path.write_text(content)
|
|
248
|
+
|
|
249
|
+
loaded = load_config(base_dir=tmp_path, config_dir=config_dir)
|
|
250
|
+
|
|
251
|
+
assert loaded.server_url == "http://test"
|
|
252
|
+
assert loaded.capture_framerate == 4
|
|
253
|
+
assert config_path.exists()
|
|
254
|
+
assert config_path.read_text() == content
|
|
@@ -39,8 +39,8 @@ class TestSegmentDirStructure:
|
|
|
39
39
|
|
|
40
40
|
def test_restore_token_path(self, tmp_path: Path):
|
|
41
41
|
config = Config(base_dir=tmp_path)
|
|
42
|
+
assert config.restore_token_path == config.config_dir / "restore_token"
|
|
42
43
|
assert str(config.restore_token_path).endswith("restore_token")
|
|
43
|
-
assert "config" in str(config.restore_token_path)
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
class TestPauseResumeState:
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
-
# Copyright (c) 2026 sol pbc
|
|
3
|
-
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
from solstone_linux.config import Config, load_config, save_config
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class TestConfig:
|
|
10
|
-
def test_defaults(self):
|
|
11
|
-
config = Config()
|
|
12
|
-
assert config.server_url == ""
|
|
13
|
-
assert config.key == ""
|
|
14
|
-
assert config.segment_interval == 300
|
|
15
|
-
|
|
16
|
-
def test_captures_dir(self):
|
|
17
|
-
config = Config()
|
|
18
|
-
assert config.captures_dir == config.base_dir / "captures"
|
|
19
|
-
|
|
20
|
-
def test_restore_token_path(self):
|
|
21
|
-
config = Config()
|
|
22
|
-
assert config.restore_token_path == config.base_dir / "config" / "restore_token"
|
|
23
|
-
|
|
24
|
-
def test_round_trip(self, tmp_path: Path):
|
|
25
|
-
config = Config(base_dir=tmp_path)
|
|
26
|
-
config.server_url = "https://example.com"
|
|
27
|
-
config.key = "test-key-123"
|
|
28
|
-
config.stream = "archon"
|
|
29
|
-
config.segment_interval = 600
|
|
30
|
-
|
|
31
|
-
save_config(config)
|
|
32
|
-
|
|
33
|
-
loaded = load_config(tmp_path)
|
|
34
|
-
assert loaded.server_url == "https://example.com"
|
|
35
|
-
assert loaded.key == "test-key-123"
|
|
36
|
-
assert loaded.stream == "archon"
|
|
37
|
-
assert loaded.segment_interval == 600
|
|
38
|
-
|
|
39
|
-
def test_load_missing(self, tmp_path: Path):
|
|
40
|
-
config = load_config(tmp_path)
|
|
41
|
-
assert config.server_url == ""
|
|
42
|
-
assert config.key == ""
|
|
43
|
-
|
|
44
|
-
def test_load_corrupt(self, tmp_path: Path):
|
|
45
|
-
config_dir = tmp_path / "config"
|
|
46
|
-
config_dir.mkdir(parents=True)
|
|
47
|
-
(config_dir / "config.json").write_text("not json!")
|
|
48
|
-
|
|
49
|
-
config = load_config(tmp_path)
|
|
50
|
-
assert config.server_url == ""
|
|
51
|
-
|
|
52
|
-
def test_permissions(self, tmp_path: Path):
|
|
53
|
-
config = Config(base_dir=tmp_path)
|
|
54
|
-
config.server_url = "https://example.com"
|
|
55
|
-
config.key = "secret"
|
|
56
|
-
save_config(config)
|
|
57
|
-
|
|
58
|
-
mode = config.config_path.stat().st_mode & 0o777
|
|
59
|
-
assert mode == 0o600
|
|
60
|
-
|
|
61
|
-
def test_sync_config_roundtrip(self, tmp_path: Path):
|
|
62
|
-
config = Config(base_dir=tmp_path)
|
|
63
|
-
config.sync_retry_delays = [10, 60, 300]
|
|
64
|
-
config.sync_max_retries = 5
|
|
65
|
-
save_config(config)
|
|
66
|
-
|
|
67
|
-
loaded = load_config(tmp_path)
|
|
68
|
-
assert loaded.sync_retry_delays == [10, 60, 300]
|
|
69
|
-
assert loaded.sync_max_retries == 5
|
|
70
|
-
|
|
71
|
-
def test_cache_retention_days_roundtrip(self, tmp_path: Path):
|
|
72
|
-
config = Config(base_dir=tmp_path)
|
|
73
|
-
config.cache_retention_days = 14
|
|
74
|
-
save_config(config)
|
|
75
|
-
|
|
76
|
-
loaded = load_config(tmp_path)
|
|
77
|
-
assert loaded.cache_retention_days == 14
|
|
78
|
-
|
|
79
|
-
def test_cache_retention_days_default(self, tmp_path: Path):
|
|
80
|
-
"""Existing configs without cache_retention_days default to 7."""
|
|
81
|
-
config_dir = tmp_path / "config"
|
|
82
|
-
config_dir.mkdir(parents=True)
|
|
83
|
-
(config_dir / "config.json").write_text('{"server_url": "http://test"}')
|
|
84
|
-
|
|
85
|
-
loaded = load_config(tmp_path)
|
|
86
|
-
assert loaded.cache_retention_days == 7
|
|
87
|
-
|
|
88
|
-
def test_capture_framerate_default(self):
|
|
89
|
-
config = Config()
|
|
90
|
-
assert config.capture_framerate == 1
|
|
91
|
-
|
|
92
|
-
def test_draw_cursor_default(self):
|
|
93
|
-
config = Config()
|
|
94
|
-
assert config.draw_cursor is True
|
|
95
|
-
|
|
96
|
-
def test_capture_framerate_roundtrip(self, tmp_path: Path):
|
|
97
|
-
config = Config(base_dir=tmp_path)
|
|
98
|
-
config.capture_framerate = 2
|
|
99
|
-
save_config(config)
|
|
100
|
-
|
|
101
|
-
loaded = load_config(tmp_path)
|
|
102
|
-
assert loaded.capture_framerate == 2
|
|
103
|
-
|
|
104
|
-
def test_draw_cursor_roundtrip(self, tmp_path: Path):
|
|
105
|
-
config = Config(base_dir=tmp_path)
|
|
106
|
-
config.draw_cursor = False
|
|
107
|
-
save_config(config)
|
|
108
|
-
|
|
109
|
-
loaded = load_config(tmp_path)
|
|
110
|
-
assert loaded.draw_cursor is False
|
|
111
|
-
|
|
112
|
-
def test_capture_framerate_defaults_on_old_config(self, tmp_path: Path):
|
|
113
|
-
"""Existing configs without capture_framerate default to 1."""
|
|
114
|
-
config_dir = tmp_path / "config"
|
|
115
|
-
config_dir.mkdir(parents=True)
|
|
116
|
-
(config_dir / "config.json").write_text('{"server_url": "http://test"}')
|
|
117
|
-
|
|
118
|
-
loaded = load_config(tmp_path)
|
|
119
|
-
assert loaded.capture_framerate == 1
|
|
120
|
-
assert loaded.draw_cursor is True
|
|
121
|
-
|
|
122
|
-
def test_capture_framerate_clamped_to_max(self, tmp_path: Path):
|
|
123
|
-
config_dir = tmp_path / "config"
|
|
124
|
-
config_dir.mkdir(parents=True)
|
|
125
|
-
(config_dir / "config.json").write_text('{"capture_framerate": 999}')
|
|
126
|
-
|
|
127
|
-
loaded = load_config(tmp_path)
|
|
128
|
-
assert loaded.capture_framerate == 10
|
|
129
|
-
|
|
130
|
-
def test_capture_framerate_clamped_to_min(self, tmp_path: Path):
|
|
131
|
-
config_dir = tmp_path / "config"
|
|
132
|
-
config_dir.mkdir(parents=True)
|
|
133
|
-
(config_dir / "config.json").write_text('{"capture_framerate": 0}')
|
|
134
|
-
|
|
135
|
-
loaded = load_config(tmp_path)
|
|
136
|
-
assert loaded.capture_framerate == 1
|
|
137
|
-
|
|
138
|
-
def test_start_paused_default(self):
|
|
139
|
-
config = Config()
|
|
140
|
-
assert config.start_paused is False
|
|
141
|
-
|
|
142
|
-
def test_start_paused_roundtrip(self, tmp_path: Path):
|
|
143
|
-
config = Config(base_dir=tmp_path)
|
|
144
|
-
config.start_paused = True
|
|
145
|
-
save_config(config)
|
|
146
|
-
|
|
147
|
-
loaded = load_config(tmp_path)
|
|
148
|
-
assert loaded.start_paused is True
|
|
149
|
-
|
|
150
|
-
def test_start_paused_defaults_on_old_config(self, tmp_path: Path):
|
|
151
|
-
"""Existing configs without start_paused default to False."""
|
|
152
|
-
config_dir = tmp_path / "config"
|
|
153
|
-
config_dir.mkdir(parents=True)
|
|
154
|
-
(config_dir / "config.json").write_text('{"server_url": "http://test"}')
|
|
155
|
-
|
|
156
|
-
loaded = load_config(tmp_path)
|
|
157
|
-
assert loaded.start_paused is False
|
|
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.3.3 → solstone_linux-0.4.1}/tests/test_observer_emits_stream_silent_event.py
RENAMED
|
File without changes
|
|
File without changes
|
{solstone_linux-0.3.3 → solstone_linux-0.4.1}/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
|