feat(workspace): full server + sync implementation, all tests green

- solitaire_server: Axum auth, sync push/pull, leaderboard, daily
  challenge, account deletion, JWT middleware, rate limiting via
  tower_governor, SQLite migrations, health endpoint
- solitaire_server: expose build_test_router (no rate limiting) so
  integration tests work without a peer IP in oneshot requests
- solitaire_sync: SyncPayload, merge logic, shared API types
- solitaire_data: SyncProvider trait, LocalOnlyProvider,
  SolitaireServerClient, auth_tokens keyring integration, blanket
  Box<dyn SyncProvider> impl
- solitaire_data/settings: derive Default on SyncBackend (clippy fix)
- .sqlx/: offline query cache so server compiles without a live DB
- sqlx: removed non-existent "offline" feature flag
- keyring v2: fixed Entry::new() returning Result<Entry>
- sqlx 0.8: all SQLite TEXT columns wrapped in Option<T>
- Integration tests: max_connections(1) on in-memory pool so all
  connections share the same schema

All 191 tests pass; cargo clippy -D warnings clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-04-26 23:32:56 +00:00
parent 13b428b81c
commit 34ba4dc6ed
55 changed files with 4372 additions and 270 deletions
+79 -1
View File
@@ -58,10 +58,48 @@ pub fn save_stats(stats: &StatsSnapshot) -> io::Result<()> {
save_stats_to(&path, stats)
}
/// Remove any leftover `*.json.tmp` files in the app data directory.
///
/// These can be left behind if the process crashes between the write and rename
/// in an atomic save. Safe to call on startup; missing or unreadable entries
/// are silently skipped.
pub fn cleanup_orphaned_tmp_files() -> io::Result<()> {
let dir = match dirs::data_dir() {
Some(d) => d.join(APP_DIR_NAME),
None => return Ok(()),
};
if !dir.exists() {
return Ok(());
}
cleanup_tmp_files_in(&dir);
Ok(())
}
/// Inner helper: delete `*.json.tmp` entries inside `dir`.
///
/// Per-file errors (already deleted, permission denied) are silently ignored.
fn cleanup_tmp_files_in(dir: &Path) {
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path
.file_name()
.and_then(|n| n.to_str())
.map(|n| n.ends_with(".json.tmp"))
.unwrap_or(false)
{
let _ = fs::remove_file(&path);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::stats::StatsSnapshot;
use crate::stats::{StatsExt, StatsSnapshot};
use solitaire_core::game_state::DrawMode;
use std::env;
@@ -109,4 +147,44 @@ mod tests {
let stats = load_stats_from(&path);
assert_eq!(stats, StatsSnapshot::default());
}
/// Test the core cleanup logic by creating `.json.tmp` files in a temporary
/// directory, running the cleanup loop manually, and verifying removal.
#[test]
fn cleanup_removes_tmp_files() {
let dir = env::temp_dir().join("solitaire_cleanup_test");
fs::create_dir_all(&dir).expect("create test dir");
// Create a pair of .json.tmp files and one regular file that must survive.
let tmp1 = dir.join("stats.json.tmp");
let tmp2 = dir.join("progress.json.tmp");
let keep = dir.join("settings.json");
fs::write(&tmp1, b"orphan1").expect("write tmp1");
fs::write(&tmp2, b"orphan2").expect("write tmp2");
fs::write(&keep, b"{}").expect("write keep");
// Run the cleanup logic directly against our test directory.
cleanup_tmp_files_in(&dir);
assert!(!tmp1.exists(), "stats.json.tmp should have been removed");
assert!(!tmp2.exists(), "progress.json.tmp should have been removed");
assert!(keep.exists(), "settings.json must not be removed");
// Tidy up.
let _ = fs::remove_file(&keep);
let _ = fs::remove_dir(&dir);
}
/// Calling `cleanup_orphaned_tmp_files` on a box with no app data dir is a
/// no-op and must not return an error.
#[test]
fn cleanup_on_nonexistent_dir_is_ok() {
// We can't control whether the real app dir exists in the test
// environment, but the public function must at least not panic or
// return an Err when the directory is absent.
// The real implementation returns Ok(()) for missing dirs.
let result = cleanup_orphaned_tmp_files();
// The function is allowed to succeed whether or not the dir exists.
assert!(result.is_ok());
}
}