strip session-added comments
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(()),
|
||||
|
||||
@@ -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(());
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user