Skip to content

Commit 8dfb061

Browse files
committed
Fix app API typing and repair confirmation ordering
1 parent 67d2b19 commit 8dfb061

4 files changed

Lines changed: 55 additions & 26 deletions

File tree

src/timecapsulesmb/app/ops/deploy.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
verify_managed_runtime,
4444
)
4545
from timecapsulesmb.device.compat import (
46+
DeviceCompatibility,
4647
is_netbsd4_payload_family,
4748
payload_family_description,
4849
render_compatibility_message,
@@ -74,14 +75,14 @@
7475
payload_verification_error,
7576
render_flash_runtime_config,
7677
)
77-
from timecapsulesmb.services.runtime import load_env_config, resolve_validated_managed_target
78+
from timecapsulesmb.services.runtime import ManagedTargetState, load_env_config, resolve_validated_managed_target
7879
from timecapsulesmb.transport.ssh import SshCommandTimeout, SshConnection, SshError
7980

8081

8182
ACP_REBOOT_REQUEST_TIMEOUT_SECONDS = 10
8283

8384

84-
def require_supported_payload(target, *, allow_unsupported: bool) -> object:
85+
def require_supported_payload(target: ManagedTargetState, *, allow_unsupported: bool) -> DeviceCompatibility:
8586
probe_state = target.probe_state
8687
if probe_state is None:
8788
raise AppOperationError("Failed to determine remote device OS compatibility.", code="remote_error")
@@ -103,7 +104,7 @@ def load_config_and_target(
103104
*,
104105
profile: str,
105106
include_probe: bool,
106-
) -> tuple[AppConfig, object]:
107+
) -> tuple[AppConfig, ManagedTargetState]:
107108
sink.stage(operation, "load_config")
108109
config = overlay_request_credentials(load_env_config(env_path=config_path(params)), params)
109110
sink.stage(operation, "resolve_managed_target")

src/timecapsulesmb/app/ops/maintenance.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -344,15 +344,15 @@ def observe_reboot_cycle(
344344

345345
def repair_xattrs_operation(params: dict[str, object], sink: EventSink) -> OperationResult:
346346
operation = "repair-xattrs"
347-
dry_run = bool_param(params, "dry_run")
348-
sink.stage(operation, "platform_check")
349-
if sys.platform != "darwin":
350-
raise AppOperationError(
351-
"repair-xattrs must be run on macOS because it uses xattr/chflags on the mounted SMB share.",
352-
code="validation_failed",
353-
)
354347
sink.stage(operation, "validate_params")
348+
dry_run = bool_param(params, "dry_run")
355349
path = required_path_param(params, "path")
350+
recursive = bool_param(params, "recursive", True)
351+
max_depth = optional_int_param(params, "max_depth")
352+
include_hidden = bool_param(params, "include_hidden")
353+
include_time_machine = bool_param(params, "include_time_machine")
354+
fix_permissions = bool_param(params, "fix_permissions")
355+
verbose = bool_param(params, "verbose")
356356
if not dry_run:
357357
require_confirmation(
358358
params,
@@ -368,17 +368,23 @@ def repair_xattrs_operation(params: dict[str, object], sink: EventSink) -> Opera
368368
),
369369
legacy_names=("confirm_repair",),
370370
)
371+
sink.stage(operation, "platform_check")
372+
if sys.platform != "darwin":
373+
raise AppOperationError(
374+
"repair-xattrs must be run on macOS because it uses xattr/chflags on the mounted SMB share.",
375+
code="validation_failed",
376+
)
371377
config = load_optional_env_config(env_path=config_path(params))
372378
args = argparse.Namespace(
373379
path=path,
374380
dry_run=dry_run,
375381
yes=not dry_run,
376-
recursive=bool_param(params, "recursive", True),
377-
max_depth=optional_int_param(params, "max_depth"),
378-
include_hidden=bool_param(params, "include_hidden"),
379-
include_time_machine=bool_param(params, "include_time_machine"),
380-
fix_permissions=bool_param(params, "fix_permissions"),
381-
verbose=bool_param(params, "verbose"),
382+
recursive=recursive,
383+
max_depth=max_depth,
384+
include_hidden=include_hidden,
385+
include_time_machine=include_time_machine,
386+
fix_permissions=fix_permissions,
387+
verbose=verbose,
382388
)
383389
context = RepairExecutionContext(lambda stage: sink.stage(operation, stage))
384390
stdout_capture = LineLogCapture(lambda message: sink.log(operation, message, level="info"))

src/timecapsulesmb/services/runtime.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from timecapsulesmb.core.config import DEFAULTS, AppConfig, ConfigError, load_app_config, require_valid_app_config
99
from timecapsulesmb.core.net import extract_host, ipv4_literal, is_link_local_ipv4, resolve_host_ipv4s
1010
from timecapsulesmb.core.paths import resolve_app_paths
11-
from timecapsulesmb.device.compat import require_compatibility
11+
from timecapsulesmb.device.compat import DeviceCompatibility, require_compatibility
1212
from timecapsulesmb.device.probe import (
1313
ProbedDeviceState,
1414
RemoteInterfaceProbeResult,
@@ -134,7 +134,7 @@ def resolve_validated_managed_target(
134134
return ManagedTargetState(connection=connection, interface_probe=None, probe_state=probe_state)
135135

136136

137-
def require_connection_compatibility(connection: SshConnection):
137+
def require_connection_compatibility(connection: SshConnection) -> DeviceCompatibility:
138138
state = probe_connection_state(connection)
139139
return require_compatibility(
140140
state.compatibility,

tests/test_app_api.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1358,21 +1358,43 @@ def test_repair_xattrs_passes_valid_max_depth_as_int(self) -> None:
13581358
def test_repair_xattrs_requires_confirmation_for_non_dry_run(self) -> None:
13591359
collector = CollectingSink()
13601360

1361-
with mock.patch("timecapsulesmb.app.ops.maintenance.repair_xattrs_service.run_repair_structured") as runner:
1362-
rc = service.run_api_request(
1363-
{
1364-
"operation": "repair-xattrs",
1365-
"params": {"path": "/Volumes/Data", "dry_run": False},
1366-
},
1367-
collector.sink,
1368-
)
1361+
with mock.patch("timecapsulesmb.app.ops.maintenance.sys.platform", "linux"):
1362+
with mock.patch("timecapsulesmb.app.ops.maintenance.repair_xattrs_service.run_repair_structured") as runner:
1363+
rc = service.run_api_request(
1364+
{
1365+
"operation": "repair-xattrs",
1366+
"params": {"path": "/Volumes/Data", "dry_run": False},
1367+
},
1368+
collector.sink,
1369+
)
13691370

13701371
self.assertEqual(rc, 1)
13711372
error = self.assert_single_terminal_event(collector, "error")
13721373
self.assertEqual(error["code"], "confirmation_required")
13731374
self.assertEqual(error["recovery"]["title"], "Repair confirmation required")
13741375
runner.assert_not_called()
13751376

1377+
def test_repair_xattrs_checks_platform_after_confirmation(self) -> None:
1378+
collector = CollectingSink()
1379+
1380+
with mock.patch("timecapsulesmb.app.ops.maintenance.sys.platform", "linux"):
1381+
with mock.patch("timecapsulesmb.app.ops.maintenance.load_optional_env_config") as load_config:
1382+
with mock.patch("timecapsulesmb.app.ops.maintenance.repair_xattrs_service.run_repair_structured") as runner:
1383+
rc = service.run_api_request(
1384+
{
1385+
"operation": "repair-xattrs",
1386+
"params": {"path": "/Volumes/Data", "dry_run": False, "confirm_repair": True},
1387+
},
1388+
collector.sink,
1389+
)
1390+
1391+
self.assertEqual(rc, 1)
1392+
error = self.assert_single_terminal_event(collector, "error")
1393+
self.assertEqual(error["code"], "validation_failed")
1394+
self.assertEqual(error["recovery"]["title"], "repair-xattrs requires macOS")
1395+
load_config.assert_not_called()
1396+
runner.assert_not_called()
1397+
13761398
def test_helper_reads_request_and_writes_ndjson(self) -> None:
13771399
output = io.StringIO()
13781400
fake_stdin = io.StringIO('{"operation":"paths","params":{}}')

0 commit comments

Comments
 (0)