Files
claude-timemachine ffdfb1f9b6
CI / test (3.10) (push) Successful in 40s
CI / test (3.11) (push) Successful in 19s
CI / test (3.12) (push) Successful in 23s
CI / build-pyz (push) Successful in 4s
CI / release (push) Has been skipped
pivot to Python: replace Kotlin/JVM with stdlib zipapp
Reasons stacked up:
  - AV: unsigned JARs that auto-download binaries + upload files trigger
    Windows Defender false-positives more often than Python scripts
    invoked by code-signed python.exe.
  - Qt UI option: PySide6 opens a path to a real Qt UI (matching Prism's
    look) if needed later. JVM Qt bindings are abandoned.
  - frazclient already needs Python; inlining as 'import cloud_sync' is
    zero overhead vs the launcher always shelling out to java.

Implementation:
  - cloud_sync package: cli.py (argparse), creds.py, scope.py,
    restic.py (binary discovery + auto-download + sha256 verify),
    sync.py (pull/push subprocess restic).
  - pyproject.toml with hatchling backend; pip-installable.
  - Makefile builds cloud-sync.pyz via python -m zipapp (~53 KB).
  - 33 pytest tests, stdlib only on runtime.
  - CI workflow runs pytest matrix (3.10/3.11/3.12) + builds pyz.
  - DESIGN.md + README.md updated to reflect Python.

E2E verified against local restic-rest-server:
  pull empty → push initial → rm -rf local → pull restores → modify+push
  creates second snapshot → client forget --prune blocked by --append-only.

Throws away ~565 LOC of Kotlin (and 18 jar tests) committed earlier in
this same session. Net result is ~250 LOC Python + 33 tests = smaller
and more aligned with the rest of the stack.
2026-06-03 01:11:47 +02:00

71 lines
2.0 KiB
Python

"""Scope file reader + materializer tests."""
from __future__ import annotations
import json
from cloud_sync.scope import (
DEFAULT_EXCLUDE,
DEFAULT_INCLUDE,
Scope,
load,
materialize_for_restic,
)
def test_load_missing_returns_defaults(tmp_path):
s = load(tmp_path)
assert s.include == DEFAULT_INCLUDE
assert s.exclude == DEFAULT_EXCLUDE
def test_load_valid_overrides_defaults(tmp_path):
state = tmp_path / ".cloud-sync"
state.mkdir()
(state / "scope.json").write_text(json.dumps({
"include": ["foo/", "bar.txt"],
"exclude": ["**/*.log"],
}))
s = load(tmp_path)
assert s.include == ["foo/", "bar.txt"]
assert s.exclude == ["**/*.log"]
def test_load_partial_keeps_defaults_for_missing(tmp_path):
state = tmp_path / ".cloud-sync"
state.mkdir()
(state / "scope.json").write_text(json.dumps({"include": ["just-this"]}))
s = load(tmp_path)
assert s.include == ["just-this"]
assert s.exclude == DEFAULT_EXCLUDE
def test_load_invalid_falls_back(tmp_path, capsys):
state = tmp_path / ".cloud-sync"
state.mkdir()
(state / "scope.json").write_text("{not valid json")
s = load(tmp_path)
assert s.include == DEFAULT_INCLUDE
captured = capsys.readouterr()
assert "invalid" in captured.err.lower()
def test_materialize_writes_files(tmp_path):
scope = Scope(include=["config/", "options.txt"], exclude=["**/*.log"])
files_from, exclude_from = materialize_for_restic(tmp_path, scope)
assert files_from.exists()
assert exclude_from.exists()
body_in = files_from.read_text().splitlines()
body_ex = exclude_from.read_text().splitlines()
# trailing slash stripped on include entries
assert "config" in body_in
assert "options.txt" in body_in
assert "**/*.log" in body_ex
def test_materialize_creates_state_dir(tmp_path):
scope = Scope(include=["x"], exclude=["y"])
files_from, _ = materialize_for_restic(tmp_path, scope)
assert files_from.parent.name == ".cloud-sync"
assert files_from.parent.exists()