Skip to content

fix: address bugs and add test coverage for save-sync#3135

Merged
gantoine merged 6 commits intorommapp:save-syncfrom
tmgast:save-sync-cleanup
Mar 16, 2026
Merged

fix: address bugs and add test coverage for save-sync#3135
gantoine merged 6 commits intorommapp:save-syncfrom
tmgast:save-sync-cleanup

Conversation

@tmgast
Copy link
Copy Markdown
Member

@tmgast tmgast commented Mar 16, 2026

Summary

Cleanup pass on the save-sync branch addressing correctness issues, a security concern, and test coverage gaps found during review.

Bug fixes

  • Sync watcher non-functional in production: _extract_device_and_platform used os.path.relpath(path) (relative to CWD, which is /backend), so every incoming file was silently ignored. Fixed to compute paths relative to the sync base directory. Also unified build_* methods to return relative paths consistently -- callers prepend base_path as needed.
  • SSH connection leak: conn.close() in push-pull task was only on the success path. Moved to finally block.
  • Race condition in session counter: saves.py read operations_completed, then wrote +1 -- concurrent uploads lose counts. Replaced with atomic SQL SET operations_completed = operations_completed + 1.
  • Orphaned sync session: trigger_push_pull created a session but never passed its ID to the background job. The job created its own, leaving the endpoint's session permanently PENDING.
  • ORM mutation in UserSchema: from_orm_with_request set current_device_id on the live SQLAlchemy object (with # type: ignore). Now sets it on the validated schema instance instead.
  • Wasteful SSH download: _process_remote_save downloaded files via SSH even when no matching server save existed, then immediately deleted them.
  • Migration PostgreSQL incompatibility: ON UPDATE CURRENT_TIMESTAMP is MySQL-only DDL. Conditioned on dialect. Also added missing enum drop in downgrade().

Security

  • SSH credentials in API response: DeviceSchema serialized sync_config including ssh_password and ssh_key_path to any client with DEVICES_READ scope. Added field serializer that masks these with a fixed-length "********".

Convention fixes

  • Replaced three session.query() calls with select() API in sync_sessions_handler
  • Fixed from pydantic import BaseModel collision in sync.py (should use project base)
  • Renamed rom_user_status_enum variable in migration (copy-paste from another migration)
  • Extracted duplicated session counter block in saves.py into _increment_session_counter helper
  • Added exc_info=True to swallowed exception warnings
  • Added log.warning for disabled SSH host key verification (was a silent # TODO)

Tests (21 new)

  • Sync watcher path parsing: valid, non-incoming, too few parts, nested, outside base
  • Conflicts directory creation and idempotency
  • increment_operations_completed: atomic counter, no-op on missing session
  • NoResultFound raised on update/complete/fail with nonexistent session
  • Negotiate edge cases: untracked saves, server-only saves, deleted-by-client detection, FAILED/CANCELLED session rejection
  • trigger_push_pull passes session_id in enqueue kwargs
  • SSH credential masking in device response (with and without sensitive fields, null config)
  • create_or_find_web_device: creation, fingerprint matching, last_seen update
  • _get_device_name UA parsing variants

Test plan

  • Full suite: 1113 passed, 0 failed, 1 skipped

tmgast added 3 commits March 16, 2026 10:56
…-sync

- Fix broken path construction in FSSyncHandler: build_* methods now
  return relative paths; sync_watcher uses paths relative to sync base
  instead of CWD (was completely non-functional in production)
- Fix SSH connection leak in push-pull task: conn.close() now in finally
- Add log.warning for disabled SSH host key verification
- Fix race condition in session operation counter: use atomic SQL
  increment instead of read-then-write
- Extract _increment_session_counter helper, add exc_info to warnings
- Replace legacy session.query() with select() in sync_sessions_handler
- Fix orphaned session: trigger_push_pull now passes session_id to job
- Fix wasteful SSH download when no matched_save exists
- Fix BaseModel import collision in sync.py (pydantic -> project base)
- Fix ORM mutation in UserSchema.from_orm_with_request: set field on
  schema instance instead of mutating live ORM object
- Mask ssh_password and ssh_key_path in DeviceSchema API response
- Fix migration PostgreSQL compatibility: condition ON UPDATE clause
  on MySQL, drop enum in downgrade
- Rename copy-paste artifact rom_user_status_enum
- Restore NoResultFound behavior on update_session, complete_session,
  fail_session when row is missing (scalar returns None, old .one()
  raised -- silent None is a semantic regression)
- Remove redundant get_session call from _increment_session_counter;
  the atomic SQL increment is already a no-op on missing rows
- Log warning when passed session_id is not found in _sync_device
  instead of silently creating an orphan session
…king, and auth utils

- test_sync_sessions_handler: increment_operations_completed (atomic
  counter, no-op on missing), NoResultFound on update/complete/fail
  with nonexistent session
- test_sync_watcher: _extract_device_and_platform path parsing (valid,
  non-incoming, too few parts, nested, outside base), _ensure_conflicts_dir
  creation and idempotency, process_sync_changes empty/disabled
- test_sync (endpoints): negotiate with untracked saves (no_op),
  server saves not mentioned by client (download), deleted-by-client
  detection (skip), complete on FAILED/CANCELLED session (400),
  trigger_push_pull passes session_id in enqueue kwargs
- test_device (endpoints): sync_config SSH credential masking
  (ssh_password/ssh_key_path -> "********"), null config passthrough,
  config without sensitive fields
- test_utils_auth: _get_device_name UA parsing (browser+OS, browser
  only, OS only, neither), create_or_find_web_device (creates new,
  returns existing on fingerprint match, updates last_seen)
Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

uv sync installs sqlalchemy[mariadb-connector] regardless of which
database is being tested, so libmariadb-dev must be present on all
runners. The postgres migration job and pytest postgresql matrix were
missing this step.
Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

The syncsessionstatus enum creation used checkfirst=False which fails
with DuplicateObject if the type already exists (e.g., test reruns or
partial migrations). Matches the pattern used in the downgrade.
Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

… PostgreSQL

The sa.Enum() inline in create_table tried to CREATE TYPE again after
the explicit ENUM.create() call. Use the pre-created enum variable
with create_type=False for PostgreSQL to avoid DuplicateObject error.

Verified locally: upgrade, downgrade, re-upgrade all clean on PostgreSQL.
Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@gantoine gantoine merged commit 318d5c8 into rommapp:save-sync Mar 16, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants