snapshot of https://github.com/ActivityWatch/aw-server-rust.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build / ubuntu-latest (push) Failing after 5s
Build / Android (push) Failing after 2s
Build / Code coverage (push) Failing after 2s
Lint / format (push) Failing after 3s
Lint / clippy (push) Failing after 3s
Build / macOS-latest (push) Has been cancelled
Build / windows-latest (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 00:19:17 +05:00
commit da934b693c
120 changed files with 26659 additions and 0 deletions

View File

@@ -0,0 +1,164 @@
/// Regression test for the "synced-from-<own hostname>" duplication reported in
/// https://github.com/orgs/ActivityWatch/discussions/1373
///
/// aw-sync already refuses to import its *own* export (`find_remotes_nonlocal`
/// filters the local device_id out of the remote list). That guard only covers
/// the direct path. It does not cover the case where a host's data is laundered
/// through a *peer*: the peer imports HOSTA's buckets, then re-exports them as
/// part of its own push, and HOSTA imports them back as
/// `<bucket>_HOSTA-synced-from-HOSTA` sitting next to the real local bucket.
///
/// Both copies then render in /timeline, so every event is shown twice.
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use aw_datastore::Datastore;
use aw_models::{Bucket, BucketMetadata};
use aw_sync::{sync_datastores, AccessMethod, SyncSpec};
// Tests in this binary run in parallel. Use a monotonic counter to guarantee
// each datastore gets a unique path even when two tests start within the same
// clock tick (Windows/macOS SystemTime resolution can be coarser than 1 ns,
// causing path collisions: "duplicate column name" on macOS, "database is
// locked" on Windows).
static DB_COUNTER: AtomicU64 = AtomicU64::new(0);
fn tmp_db(name: &str) -> PathBuf {
let mut p = std::env::temp_dir();
// Counter: within-run uniqueness. Timestamp: cross-run uniqueness when
// the PID is reused and the counter resets to 0. Same as #655.
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
p.push(format!(
"aw-sync-roundtrip-{}-{}-{}-{}.db",
std::process::id(),
name,
DB_COUNTER.fetch_add(1, Ordering::Relaxed),
ts,
));
p
}
fn datastore(name: &str) -> Datastore {
Datastore::new(tmp_db(name).to_str().unwrap().to_string(), false)
}
fn bucket(id: &str, hostname: &str) -> Bucket {
Bucket {
bid: None,
id: id.to_string(),
_type: "currentwindow".to_string(),
client: "aw-watcher-window".to_string(),
hostname: hostname.to_string(),
created: None,
data: serde_json::Map::new(),
metadata: BucketMetadata::default(),
events: None,
last_updated: None,
}
}
fn bucket_ids(ds: &dyn AccessMethod) -> Vec<String> {
let mut ids: Vec<String> = ds.get_buckets().unwrap().keys().cloned().collect();
ids.sort();
ids
}
/// Two hosts sharing a sync folder. Returns (a_local, b_export) after a full
/// push/pull round: HOSTA pushes, HOSTB pulls, HOSTB pushes, HOSTA pulls.
fn round_trip() -> (Datastore, Datastore) {
let spec = SyncSpec::default();
// HOSTA: local server datastore + the db it pushes into the sync folder.
let a_local = datastore("a-local");
let a_export = datastore("a-export");
// HOSTB: same.
let b_local = datastore("b-local");
let b_export = datastore("b-export");
// HOSTA collects some data locally.
a_local
.create_bucket(&bucket("aw-watcher-window_HOSTA", "HOSTA"))
.unwrap();
// 1. HOSTA pushes to its own folder in the sync dir.
sync_datastores(&a_local, &a_export, true, Some("device-A"), &spec).unwrap();
// 2. HOSTB pulls HOSTA's export. This copy is correct and expected.
sync_datastores(&a_export, &b_local, false, None, &spec).unwrap();
assert!(
bucket_ids(&b_local).contains(&"aw-watcher-window_HOSTA-synced-from-HOSTA".to_string()),
"precondition: HOSTB should hold HOSTA's data as a synced-from-HOSTA bucket, got {:?}",
bucket_ids(&b_local)
);
// 3. HOSTB pushes its own data to the sync folder.
sync_datastores(&b_local, &b_export, true, Some("device-B"), &spec).unwrap();
// 4. HOSTA pulls HOSTB's export.
sync_datastores(&b_export, &a_local, false, None, &spec).unwrap();
(a_local, b_export)
}
/// The fix location: a push must export only buckets that originate on this
/// host. Re-exporting buckets pulled from a peer is what lets data round-trip.
#[test]
fn test_push_does_not_reexport_synced_buckets() {
let (_a_local, b_export) = round_trip();
assert_eq!(
bucket_ids(&b_export),
Vec::<String>::new(),
"HOSTB's export must not contain buckets it merely synced from HOSTA"
);
}
/// The reported symptom: HOSTA must never end up with a `-synced-from-HOSTA`
/// copy of its own bucket, which renders every event twice in /timeline.
#[test]
fn test_own_data_does_not_return_via_peer() {
let (a_local, _b_export) = round_trip();
assert_eq!(
bucket_ids(&a_local),
vec!["aw-watcher-window_HOSTA".to_string()],
"HOSTA must not gain a synced-from-HOSTA copy of its own bucket"
);
}
/// `-synced-from-` is a reserved token in aw-sync's ID grammar, not just a
/// naming convention. `get_or_create_sync_bucket` splits on it to recover the
/// original bucket ID, so a first-hand bucket whose own ID contains it is
/// already mangled on import regardless of provenance filtering.
///
/// This pins that pre-existing behaviour so the reservation is explicit: such a
/// bucket is treated as a synced copy. Issue #649 tracks moving provenance into
/// bucket metadata, which removes the dependency on the ID string.
#[test]
fn test_synced_from_is_a_reserved_id_token() {
let spec = SyncSpec::default();
let local = datastore("reserved-local");
let export = datastore("reserved-export");
// A bucket the sync layer would mangle anyway: the ID grammar reserves the
// token, so this is not a supported first-hand bucket name.
local
.create_bucket(&bucket(
"aw-watcher-window_HOSTA-synced-from-HOSTB",
"HOSTA",
))
.unwrap();
// A normally-named bucket alongside it, to prove the filter is not blanket.
local
.create_bucket(&bucket("aw-watcher-afk_HOSTA", "HOSTA"))
.unwrap();
sync_datastores(&local, &export, true, Some("device-A"), &spec).unwrap();
assert_eq!(
bucket_ids(&export),
vec!["aw-watcher-afk_HOSTA".to_string()],
"the reserved token marks a bucket as synced; unmarked buckets still export"
);
}