strip session-added comments
Some checks failed
CI / Check / Test (push) Has been cancelled
CI / Check / Test (pull_request) Has been cancelled

This commit is contained in:
2026-10-26 10:32:38 +01:00
parent a270632e87
commit f020108024
138 changed files with 0 additions and 719 deletions

View File

@@ -20,7 +20,6 @@ pub async fn execute(deps: &DeleteReviewDeps, cmd: DeleteReviewCommand) -> Resul
}
let movie_id = review.movie_id().clone();
// Read before the write: the orphan event carries the poster to clean up.
let poster_path = deps
.diary
.get_review_history(&movie_id)
@@ -29,7 +28,6 @@ pub async fn execute(deps: &DeleteReviewDeps, cmd: DeleteReviewCommand) -> Resul
.poster_path()
.cloned();
// One write: the review, plus its movie when this was the last review.
deps.review
.delete_review_and_orphaned_movie(
&review_id,

View File

@@ -76,7 +76,6 @@ impl ReviewLogger for DefaultReviewLogger {
watched_at: *review.watched_at(),
};
// Events ride the review save (the decisive write) so they commit with it.
let mut events = Vec::with_capacity(2);
if let Some(ext_id) = movie.external_metadata_id() {
events.push(DomainEvent::MovieEnrichmentRequested {
@@ -87,7 +86,6 @@ impl ReviewLogger for DefaultReviewLogger {
events.push(review_event);
self.review_repo.save_review(&review, &events).await?;
// The removal event is recorded only if an entry was actually removed.
self.watchlist_repo
.remove_if_present(
review.user_id(),

View File

@@ -29,7 +29,6 @@ pub async fn execute(
cmd.target_count,
GoalType::Movies,
)?;
// Reads that can fail happen before the write, so an error never follows a commit.
let current_count = deps.stats.count_reviews_in_year(&user_id, cmd.year).await?;
let event = DomainEvent::GoalCreated {
goal_id: g.id().clone(),

View File

@@ -17,7 +17,6 @@ pub async fn execute(
.ok_or_else(|| DomainError::NotFound(format!("Goal for year {}", cmd.year)))?;
g.update_target(cmd.target_count)?;
// Reads that can fail happen before the write, so an error never follows a commit.
let current_count = deps.stats.count_reviews_in_year(&user_id, cmd.year).await?;
let event = DomainEvent::GoalUpdated {
goal_id: g.id().clone(),

View File

@@ -88,7 +88,6 @@ pub async fn execute(
})
}
/// Row-level validation failures keep their bare message in the summary.
fn import_error_message(e: DomainError) -> String {
match e {
DomainError::ValidationError(msg) => msg,

View File

@@ -7,8 +7,6 @@ use domain::{
ports::{PeriodicJob, ProcessedEventLedger},
};
/// Ledger rows older than this can no longer matter: the queue gives up on an
/// event long before, so it cannot be redelivered.
pub const PROCESSED_EVENTS_RETENTION_DAYS: i64 = 30;
pub struct ProcessedEventsCleanupJob {

View File

@@ -7,9 +7,6 @@ use domain::{
ports::{AccountPurger, PeriodicJob},
};
/// How long a purged account's stripped user row and signing keypair stay.
/// Federation deliveries of the account's `Delete` are signed from them, and
/// their retries are long over by then.
pub const TOMBSTONE_RETENTION_DAYS: i64 = 7;
pub struct TombstoneCleanupJob {

View File

@@ -14,8 +14,6 @@ pub async fn execute(deps: &EnrichMovieDeps, cmd: EnrichMovieCommand) -> Result<
}
let Some(movie) = deps.movie_query.get_movie_by_id(&cmd.movie_id).await? else {
// The movie row is written with the event, so a missing row means it was
// deleted. Do not store the profile: it is the "enrichment done" marker.
tracing::warn!(movie_id = %cmd.movie_id.value(), "enrich_movie: movie not found, skipping");
return Ok(());
};
@@ -37,8 +35,6 @@ pub async fn execute(deps: &EnrichMovieDeps, cmd: EnrichMovieCommand) -> Result<
.await?;
}
// Written last: a stored fresh profile is what makes a redelivered
// enrichment skip, so it must only exist once every other effect landed.
deps.movie_profile.upsert(&cmd.profile).await?;
tracing::info!(

View File

@@ -110,10 +110,6 @@ impl EventHandler for PosterSyncHandler {
movie_id,
external_metadata_id,
} => {
// The poster is stored under the movie id (the converter later
// appends `.webp`/`.avif`) and the movie row is updated
// atomically with `PosterSynced`; exactly that path on the movie
// means an earlier delivery completed.
let id_str = movie_id.value().to_string();
let canonical = [
id_str.clone(),

View File

@@ -2,8 +2,6 @@ use domain::{errors::DomainError, events::DomainEvent, value_objects::UserId};
use super::{commands::SocialCmd, deps::SocialCommandDeps};
/// The command's event rides the write: the social port records it with the
/// change (atomically where the adapter owns the transaction).
pub async fn execute_command(deps: &SocialCommandDeps, cmd: SocialCmd) -> Result<(), DomainError> {
let command = &deps.social_command;
match cmd {

View File

@@ -2,8 +2,6 @@ use domain::{errors::DomainError, value_objects::UserId};
use crate::users::deps::EnsureActiveDeps;
/// A token outlives its account; this is the check that stops a deleted
/// (or vanished) user from acting on one.
pub async fn execute(deps: &EnsureActiveDeps, user: &UserId) -> Result<(), DomainError> {
match deps.user.find_by_id(user).await? {
Some(_) => Ok(()),

View File

@@ -9,21 +9,6 @@ use domain::{
},
};
/// Purges a deleted account once its `UserDeleted` event arrives.
///
/// Order matters. Federation is told first (deliveries are only enqueued, and
/// signed later from the tombstone the purge leaves behind). A notifier
/// failure never blocks the purge: unreachable servers must not keep a
/// person's data. Then the database rows go in one transaction, and the
/// image objects after the commit. Image references are dropped only once the
/// objects are gone, so a crash in between is recovered by the redelivery.
///
/// Idempotent: every step is a no-op on a re-run, and the notification is
/// skipped once the account is purged.
///
/// A purge that fails after the notification was queued is redelivered and
/// queues the `Delete` again. That is harmless: a repeated `Delete` of an
/// already deleted actor is idempotent on remote servers.
pub struct AccountPurgeHandler {
user: Arc<dyn UserRepository>,
notifier: Arc<dyn FederationDeletionNotifier>,
@@ -57,7 +42,6 @@ impl EventHandler for AccountPurgeHandler {
let DomainEvent::UserDeleted { user_id } = event else {
return Ok(());
};
// Gone already (removed by the tombstone cleanup): nothing left to do.
if self.user.find_tombstoned_by_id(user_id).await?.is_none() {
return Ok(());
}

View File

@@ -2,10 +2,6 @@ use domain::{errors::DomainError, models::UserRole, value_objects::UserId};
use crate::users::deps::RequestAccountDeletionDeps;
/// Tombstones `target`. A user may delete themselves; an admin may delete
/// anyone. Either way the *actor's* password is the proof, and the last
/// active admin can never be removed. `UserDeleted` rides the repository
/// write, so the tombstone and its event commit together.
pub async fn execute(
deps: &RequestAccountDeletionDeps,
actor: UserId,

View File

@@ -19,8 +19,6 @@ pub struct WorkerService {
}
impl WorkerService {
/// Fails if two handlers share a name: the name keys the processed-event
/// ledger, so a collision would make one handler's success skip the other.
pub fn new(
consumer: Arc<dyn EventConsumer>,
handlers: Vec<Arc<dyn EventHandler>>,
@@ -89,8 +87,6 @@ impl WorkerService {
}
}
/// Margin added to a claim's remaining lease before a blocked delivery is
/// retried, so the retry lands after the lease has lapsed rather than on it.
const DEFER_MARGIN: Duration = Duration::from_secs(5);
async fn dispatch(
@@ -100,7 +96,6 @@ async fn dispatch(
) {
let id = envelope.id().copied();
let mut any_transient = false;
// Longest wait among handlers blocked behind another delivery's live claim.
let mut blocked_for: Option<Duration> = None;
for handler in handlers.iter() {
let name = handler.name();
@@ -118,7 +113,6 @@ async fn dispatch(
continue;
}
Err(e) => {
// Running anyway could repeat a side effect; retry the delivery instead.
tracing::warn!(handler = name, "ledger claim failed, nacking delivery: {e}");
any_transient = true;
continue;
@@ -131,15 +125,10 @@ async fn dispatch(
if let (Some(id), Some(token)) = (&id, &claim)
&& let Err(e) = ledger.complete(id, name, token).await
{
// The work is done; failing the delivery would only repeat it.
// The claim lapses after its lease and the handler may run once more.
tracing::warn!(handler = name, "completing ledger claim failed: {e}");
}
}
Err(e) => {
// Permanent failures are released too, not recorded: if a sibling
// handler's transient failure redelivers this event, a handler that
// failed permanently runs (and fails) again.
if let (Some(id), Some(token)) = (&id, &claim)
&& let Err(le) = ledger.release(id, name, token).await
{
@@ -160,10 +149,6 @@ async fn dispatch(
}
}
}
// A blocked handler cannot be retried before its claim is done or lapsed,
// so wait that long without spending a retry attempt; any transient failure
// in the same delivery is retried then too. A plain nack would burn the
// retry budget (and dead-letter) long before a crashed claim's lease ends.
if let Some(wait) = blocked_for {
if let Err(e) = envelope.defer(wait + DEFER_MARGIN).await {
tracing::error!("defer failed: {e}");

View File

@@ -17,9 +17,6 @@ pub async fn execute(
tracing::debug!("wrapup {} already ready, skipping", wrapup_id.value());
return Ok(());
}
// A record still `Generating` is an earlier delivery that died mid-way. The
// ledger guarantees no concurrent run, so resume it: generation is a pure
// read of stats and `set_complete` is the single atomic completion write.
deps.wrapup_repo
.update_status(&wrapup_id, &WrapUpStatus::Generating, None)

View File

@@ -90,7 +90,6 @@ async fn registration_refuses_a_retired_username() {
.unwrap()
.unwrap();
users.mark_deleted(old.id(), &[]).await.unwrap();
// The purge removes the row; only the retired name keeps the username taken.
users.delete_user(old.id());
let err = register::execute(&deps, register_command("new@example.com", "Ghost"))

View File

@@ -78,7 +78,6 @@ async fn edit_other_users_review_returns_forbidden() {
assert!(matches!(err, domain::errors::DomainError::Forbidden(_)));
}
/// Loads from the store but refuses every write.
pub(crate) struct WriteRefusingReviews(pub(crate) Arc<InMemoryStore>);
#[async_trait::async_trait]

View File

@@ -1,6 +1,3 @@
//! Every event handler may run twice for the same event (lease takeover after
//! a crash mid-handler). Running it twice must leave the world as if it ran once.
use std::collections::HashMap;
use std::sync::{
Arc, Mutex,
@@ -36,7 +33,6 @@ use application::person::event_handler::PersonEnrichmentHandler;
// ── fakes ───────────────────────────────────────────────────────────────────
/// Search index keyed like a real one: indexing the same id twice is one doc.
#[derive(Default)]
struct RecordingSearch {
docs: Mutex<HashMap<(String, String), u32>>,
@@ -79,7 +75,6 @@ impl SearchCommand for RecordingSearch {
}
}
/// Fails the first `index` call, succeeds afterwards.
struct FlakySearch {
inner: Arc<RecordingSearch>,
failed_once: AtomicUsize,
@@ -103,7 +98,6 @@ fn person(tmdb: u64, name: &str) -> Person {
Person::basic(PersonId::from_external(&ext), ext, name.into(), None, None)
}
/// One person whose `enriched_at` is set by `update_enrichment`, like the real store.
struct FakePersons {
person: Mutex<Person>,
enrich_writes: AtomicUsize,
@@ -239,7 +233,6 @@ impl domain::ports::ImageFetcher for NoImages {
}
}
/// Returns the key it was given, like the real adapter.
struct KeyStorage;
#[async_trait]
@@ -371,8 +364,6 @@ async fn a_movie_enrichment_that_failed_midway_is_redone_on_redelivery() {
};
assert!(handler.handle(&event).await.is_err());
// The profile row is the "enrichment done" marker: it must not exist yet,
// or the redelivery below would skip and the search index would stay empty.
assert!(store.get_by_movie_id(movie.id()).await.unwrap().is_none());
handler.handle(&event).await.unwrap();
@@ -517,13 +508,11 @@ async fn poster_sync_skips_converted_poster_keys_but_not_lookalike_paths() {
external_metadata_id: ext,
};
// The converter's output is the canonical synced key: skipped.
movie.update_poster(PosterPath::new(format!("{id}.webp")).unwrap());
store.upsert_movie(&movie, &[]).await.unwrap();
handler.handle(&event).await.unwrap();
assert!(publisher.published().is_empty());
// A foreign path that merely contains the id is not a synced poster.
movie.update_poster(PosterPath::new(format!("https://cdn.example/{id}/p.jpg")).unwrap());
store.upsert_movie(&movie, &[]).await.unwrap();
handler.handle(&event).await.unwrap();

View File

@@ -1,7 +1,3 @@
//! Command builders for tests. Each builder returns a command with sensible
//! defaults; tests override individual fields with struct-update syntax:
//! `LogReviewCommand { rating: 6, ..log_review_command(user, movie) }`.
use chrono::NaiveDate;
use uuid::Uuid;
@@ -66,7 +62,6 @@ pub fn log_review_command_manual(user_id: Uuid, title: &str, year: u16) -> LogRe
}
}
/// Edits only the rating; `review_id` and `requesting_user_id` are random.
pub fn edit_review_command(rating: u8) -> EditReviewCommand {
EditReviewCommand {
review_id: Uuid::new_v4(),
@@ -115,7 +110,6 @@ pub fn update_goal_command(year: u16, target_count: u32) -> UpdateGoalCommand {
}
}
/// Confirms indices `[0]`.
pub fn execute_import_command(user_id: Uuid, session_id: Uuid) -> ExecuteImportCommand {
ExecuteImportCommand {
user_id,
@@ -132,7 +126,6 @@ pub fn create_import_session_command(user_id: Uuid, csv: &[u8]) -> CreateImportS
}
}
/// No field mappings.
pub fn apply_import_mapping_command(user_id: Uuid, session_id: Uuid) -> ApplyImportMappingCommand {
ApplyImportMappingCommand {
user_id,
@@ -153,7 +146,6 @@ pub fn apply_import_profile_command(
}
}
/// One confirmation, rating as given, no comment.
pub fn confirm_watch_events_command(
user_id: Uuid,
watch_event_id: Uuid,
@@ -176,7 +168,6 @@ pub fn dismiss_watch_events_command(
DismissWatchEventsCommand { user_id, event_ids }
}
/// Jellyfin provider, no label.
pub fn generate_webhook_token_command(user_id: Uuid) -> GenerateWebhookTokenCommand {
GenerateWebhookTokenCommand {
user_id,
@@ -185,7 +176,6 @@ pub fn generate_webhook_token_command(user_id: Uuid) -> GenerateWebhookTokenComm
}
}
/// Every field unset; override what the test cares about.
pub fn update_profile_command(user_id: Uuid) -> UpdateProfileCommand {
UpdateProfileCommand {
user_id,
@@ -199,7 +189,6 @@ pub fn update_profile_command(user_id: Uuid) -> UpdateProfileCommand {
}
}
/// All federation toggles on.
pub fn update_user_settings_command(user_id: Uuid) -> UpdateUserSettingsCommand {
UpdateUserSettingsCommand {
user_id,
@@ -216,7 +205,6 @@ pub fn add_to_watchlist_command(user_id: Uuid, movie_id: Uuid) -> AddToWatchlist
}
}
/// Window is 1 Jan of `year` to 1 Jan of `year + 1`.
pub fn request_wrap_up_command(user_id: Option<Uuid>, year: i32) -> RequestWrapUpCommand {
RequestWrapUpCommand {
user_id,

View File

@@ -1,6 +1,3 @@
//! Shared fakes and builders for the application integration tests.
//! Included via `mod support;` from each `tests/<module>.rs`; not every
//! binary uses every helper.
#![allow(dead_code, unused_imports)]
mod commands;

View File

@@ -139,7 +139,6 @@ fn entries(log: &Log) -> Vec<String> {
#[test]
fn the_handler_name_is_stable() {
// The name keys the processed-event ledger; renaming re-runs events.
let f = tokio::runtime::Runtime::new()
.unwrap()
.block_on(fixture(false, true));
@@ -255,9 +254,6 @@ async fn a_redelivered_event_is_skipped_by_the_ledger() {
assert_eq!(purges, 1);
}
/// The rows were committed but the worker died before deleting the image
/// objects: the redelivery finds the account purged, still sees the image
/// references, deletes the objects and clears the references.
#[tokio::test]
async fn a_rerun_after_a_crash_following_the_commit_deletes_the_images() {
let f = fixture(false, true).await;

View File

@@ -21,8 +21,6 @@ use domain::models::{User, UserSummary};
use domain::ports::UserRepository as UserRepositoryPort;
use domain::value_objects::Username;
/// Delegates to a store but reports a stale admin count, as a concurrent
/// deletion would leave the use case's pre-check.
struct StaleAdminCount(Arc<InMemoryStore>);
#[async_trait]

View File

@@ -273,7 +273,6 @@ async fn changing_also_known_as_records_the_move_with_the_update() {
assert!(matches!(recorded[1], DomainEvent::UserAccountMoved { .. }));
}
/// Loads from the store but refuses the profile write.
struct ProfileWriteRefusingUsers(Arc<InMemoryStore>);
#[async_trait::async_trait]

View File

@@ -377,7 +377,6 @@ impl EventHandler for CountingHandler {
}
}
/// Delivers the same event (same id) `times` times, recording ack/nack per delivery.
struct RedeliveringConsumer {
id: Option<domain::events::EventId>,
event: DomainEvent,
@@ -416,8 +415,6 @@ impl EventConsumer for RedeliveringConsumer {
Box::new(CountNack(Arc::clone(&nacks), Arc::clone(&defers))),
);
async move {
// Let the previous delivery's handlers finish first: a real
// broker redelivers only after the nack.
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
Ok(envelope)
}
@@ -498,7 +495,6 @@ async fn a_handler_already_running_elsewhere_is_not_run_concurrently() {
let (handler, calls) = CountingHandler::new("slow", 0);
let store = InMemoryStore::new();
let id = domain::events::EventId::new();
// Another delivery holds a live claim on this (event, handler).
assert!(matches!(
store.begin(&id, "slow").await.unwrap(),
Begin::Run(_)
@@ -560,7 +556,6 @@ async fn a_delivery_blocked_by_a_live_claim_is_deferred_without_consuming_an_att
assert_eq!(*nacks.lock().unwrap(), 0, "a nack would consume an attempt");
let defers = defers.lock().unwrap();
assert_eq!(defers.len(), 1);
// Retried only once the claim's lease has run out (plus a margin).
let lease = std::time::Duration::from_secs(HANDLER_LEASE_SECONDS as u64);
assert!(
defers[0] > lease - std::time::Duration::from_secs(5),
@@ -588,7 +583,6 @@ async fn a_lapsed_owner_cannot_release_a_claim_taken_over_by_another() {
let Begin::Run(lapsed) = store.begin(&id, "h").await.unwrap() else {
panic!("stale claim should be taken over");
};
// Simulate: the original owner's token is from before the takeover.
let original = domain::ports::ClaimToken::new("2020-01-01T00:00:00.000Z".into());
assert_ne!(original, lapsed);
@@ -652,7 +646,6 @@ async fn a_failed_handler_releases_its_claim_so_redelivery_retries() {
.run(tokio::sync::watch::channel(false).1)
.await;
// Delivery 1 fails and releases; 2 succeeds; 3 is skipped as done.
assert_eq!(*calls.lock().unwrap(), 2);
assert_eq!(*nacks.lock().unwrap(), 1);
}

View File

@@ -82,7 +82,6 @@ async fn generates_wrapup_and_marks_complete() {
assert_eq!(final_rec.status, WrapUpStatus::Ready);
assert!(final_rec.report.is_some());
// The completion event is recorded together with the completed row.
let recorded = repo.recorded_events();
assert_eq!(recorded.len(), 1);
assert!(matches!(
@@ -93,8 +92,6 @@ async fn generates_wrapup_and_marks_complete() {
#[tokio::test]
async fn a_wrapup_left_generating_is_completed_on_redelivery() {
// A crash after `Generating` was written (or a failed `set_complete`) leaves
// the record stuck; the redelivered event must finish it, not skip it.
let repo = InMemoryWrapUpRepository::new();
let wrapup_id = WrapUpId::generate();