diff --git a/crates/tracedecay-session-runtime/src/lcm_effects/tests.rs b/crates/tracedecay-session-runtime/src/lcm_effects/tests.rs index c268d65ea18..6b51b5c8caa 100644 --- a/crates/tracedecay-session-runtime/src/lcm_effects/tests.rs +++ b/crates/tracedecay-session-runtime/src/lcm_effects/tests.rs @@ -2277,6 +2277,157 @@ async fn partial_revision_invalidation_hides_replay_and_yields_to_a_due_peer() { ); } +#[tokio::test] +async fn relation_recovery_orders_tied_keys_and_preserves_the_deferred_deadline() { + let harness = RegisteredGlobalDbHarness::open("relation-recovery-page-order").await; + let db = &harness.registered; + let transaction = db.begin_write_transaction().await.unwrap(); + transaction + .execute_batch( + "WITH RECURSIVE seq(n) AS ( + VALUES(0) UNION ALL SELECT n + 1 FROM seq WHERE n < 70 + ) + INSERT INTO session_temporal_generations ( + session_id, generation, state, frozen_watermarks_json, created_at + ) SELECT printf('relation-page-%02d', n / 2), 1 + n % 2, 'building', '{}', n + FROM seq; + INSERT INTO session_relation_receipts ( + session_id, generation, scope_kind, scope_id, expected_graph_watermark, + state, created_at, recovery_state, recovery_next_attempt_at + ) SELECT session_id, generation, 'project_sessions', 'unrelated-scope', 'expected', + 'pending', 42, CASE WHEN generation = 1 THEN 'retryable' ELSE 'pending' END, + CASE WHEN created_at = 70 THEN unixepoch() + 86400 + ELSE (70 - created_at) % 3 END + FROM session_temporal_generations;", + ) + .await + .unwrap(); + transaction.commit().await.unwrap(); + let access = SessionTemporalAccess::new(&**db); + let cancellation = std::sync::Arc::new(tracedecay_graph_db::NeverCancelled); + let first = access + .recover_pending_session_relation_projection_page(65, cancellation.clone()) + .await + .unwrap(); + assert_eq!( + (first.processed, first.recovered, first.has_more), + (65, 0, true) + ); + let snapshot = db.read_snapshot().await.unwrap(); + let mut rows = snapshot + .query( + "SELECT generation, recovery_state, recovery_failure_code + FROM session_relation_receipts WHERE session_id = 'relation-page-32' + ORDER BY generation", + (), + ) + .await + .unwrap(); + let settled = rows.next().await.unwrap().unwrap(); + assert_eq!(settled.get::(0).unwrap(), 1); + assert_eq!(settled.get::(2).unwrap(), "scope_mismatch"); + let sentinel = rows.next().await.unwrap().unwrap(); + assert_eq!(sentinel.get::(0).unwrap(), 2); + assert_eq!(sentinel.get::(1).unwrap(), "pending"); + assert_eq!(sentinel.get::>(2).unwrap(), None); + drop(rows); + let mut rows = snapshot + .query( + "SELECT recovery_next_attempt_at FROM session_relation_receipts + WHERE session_id = 'relation-page-35' AND generation = 1", + (), + ) + .await + .unwrap(); + let deferred_deadline = rows.next().await.unwrap().unwrap().get::(0).unwrap(); + drop(rows); + drop(snapshot); + let rest = access + .recover_pending_session_relation_projection_page(65, cancellation.clone()) + .await + .unwrap(); + assert_eq!( + (rest.processed, rest.recovered, rest.has_more), + (5, 0, false) + ); + let idle = access + .recover_pending_session_relation_projection_page(65, cancellation) + .await + .unwrap(); + assert_eq!((idle.processed, idle.has_more), (0, false)); + assert_eq!(idle.next_retry_at_unix_seconds, Some(deferred_deadline)); +} + +#[tokio::test] +async fn relation_recovery_fragmented_deadlines_preserve_order_across_single_item_pages() { + let harness = RegisteredGlobalDbHarness::open("relation-fragmented-deadlines").await; + let db = &harness.registered; + let transaction = db.begin_write_transaction().await.unwrap(); + transaction + .execute_batch( + "WITH RECURSIVE seq(n) AS ( + VALUES(0) UNION ALL SELECT n + 1 FROM seq WHERE n < 535 + ) + INSERT INTO session_temporal_generations ( + session_id, generation, state, frozen_watermarks_json, created_at + ) SELECT printf('fragmented-relation-%04d', n), 1, 'building', '{}', n + FROM seq; + INSERT INTO session_relation_receipts ( + session_id, generation, scope_kind, scope_id, expected_graph_watermark, + state, created_at, recovery_state, recovery_next_attempt_at + ) SELECT session_id, generation, 'project_sessions', 'unrelated-scope', + 'expected', 'pending', created_at, + CASE WHEN created_at < 512 THEN 'retryable' ELSE 'pending' END, + CASE WHEN created_at < 512 THEN unixepoch() + 86400 + ELSE created_at - 511 END + FROM session_temporal_generations;", + ) + .await + .unwrap(); + transaction.commit().await.unwrap(); + + let access = SessionTemporalAccess::new(&**db); + let cancellation = std::sync::Arc::new(tracedecay_graph_db::NeverCancelled); + // The 512 older receipts are sleeping, while 24 newer due receipts have + // 24 distinct deadlines. No four-deadline band can cover the due set. + // This forces the fragmented fallback repeatedly, not just on page one. + for index in 0..24 { + let page = access + .recover_pending_session_relation_projection_page(1, cancellation.clone()) + .await + .unwrap(); + assert_eq!((page.processed, page.recovered), (1, 0)); + assert_eq!(page.has_more, index < 23); + + let snapshot = db.read_snapshot().await.unwrap(); + let mut rows = snapshot + .query( + "SELECT session_id FROM session_relation_receipts + WHERE recovery_state = 'permanent' ORDER BY created_at", + (), + ) + .await + .unwrap(); + let mut settled = Vec::new(); + while let Some(row) = rows.next().await.unwrap() { + settled.push(row.get::(0).unwrap()); + } + assert_eq!(settled.len(), index + 1); + let expected = format!("fragmented-relation-{:04}", index + 512); + assert_eq!(settled.last().map(String::as_str), Some(expected.as_str())); + } + + let idle = access + .recover_pending_session_relation_projection_page(1, cancellation) + .await + .unwrap(); + assert_eq!((idle.processed, idle.has_more), (0, false)); + assert!( + idle.next_retry_at_unix_seconds.is_some(), + "the sleeping prefix must retain its future wakeup" + ); +} + #[tokio::test] async fn malformed_relation_receipt_is_permanent_without_starving_summary_work() { let harness = RegisteredGlobalDbHarness::open("lcm-relation-recovery-poison").await; @@ -2460,6 +2611,179 @@ async fn malformed_relation_receipt_is_permanent_without_starving_summary_work() .is_some_and(|delay| !delay.is_zero() && delay <= std::time::Duration::from_secs(60)), "the scheduler must retain the isolated relation retry deadline" ); + let deferred = SessionTemporalAccess::new(&*db) + .recover_pending_session_relation_projection_page( + 1, + std::sync::Arc::new(tracedecay_graph_db::NeverCancelled), + ) + .await + .unwrap(); + assert_eq!(deferred.processed, 0, "a future retry is not due"); + assert!(!deferred.has_more); + assert!(deferred.next_retry_at_unix_seconds.is_some()); + + // Preserve one-sentinel-row pagination after the idle eligibility probe: + // two durable due receipts must be recovered in distinct bounded pages, + // with has_more advertising the unprocessed second receipt. + let transaction = db.begin_write_transaction().await.unwrap(); + let changed = transaction + .execute( + "UPDATE session_relation_receipts + SET state = 'pending', graph_watermark = NULL, applied_at = NULL, + recovery_state = 'pending', recovery_failure_code = NULL, + recovery_failure_count = 0, + recovery_next_attempt_at = 0 + WHERE session_id IN (?1, ?2)", + params![poison_session, healthy_session], + ) + .await + .unwrap(); + assert_eq!(changed, 2, "both durable receipts must be present"); + // The older receipts are valid rows in the registered temporal schema, + // but their deadlines are in the future. Only the two real publication + // receipts should be recovered, in creation order, on bounded pages. + let generations = transaction + .execute( + "WITH RECURSIVE seq(n) AS ( + SELECT 1 UNION ALL SELECT n + 1 FROM seq WHERE n < 256 + ) + INSERT INTO session_temporal_generations ( + session_id, generation, state, frozen_watermarks_json, created_at + ) + SELECT printf('deferred-relation-%04d', n), 1, 'building', '{}', n FROM seq", + (), + ) + .await + .unwrap(); + assert_eq!(generations, 256); + let deferred_count = transaction + .execute( + "WITH RECURSIVE seq(n) AS ( + SELECT 1 UNION ALL SELECT n + 1 FROM seq WHERE n < 256 + ) + INSERT INTO session_relation_receipts ( + session_id, generation, scope_kind, scope_id, + expected_graph_watermark, state, created_at, + recovery_state, recovery_failure_count, + recovery_next_attempt_at + ) + SELECT printf('deferred-relation-%04d', n), 1, + scope.scope_kind, scope.scope_id, 'deferred-test', + 'pending', n, 'retryable', 1, unixepoch() + 3600 + FROM seq CROSS JOIN ( + SELECT scope_kind, scope_id + FROM session_relation_receipts WHERE session_id = ?1 + LIMIT 1 + ) AS scope", + params![poison_session], + ) + .await + .unwrap(); + assert_eq!(deferred_count, 256); + let ordered = transaction + .execute( + "UPDATE session_relation_receipts + SET created_at = CASE session_id WHEN ?1 THEN 1000 ELSE 2000 END + WHERE session_id IN (?1, ?2)", + params![poison_session, healthy_session], + ) + .await + .unwrap(); + assert_eq!(ordered, 2); + transaction.commit().await.unwrap(); + + let recovered_first = SessionTemporalAccess::new(&*db) + .recover_pending_session_relation_projection_page( + 1, + std::sync::Arc::new(tracedecay_graph_db::NeverCancelled), + ) + .await + .unwrap(); + assert_eq!(recovered_first.processed, 1); + assert!(recovered_first.has_more, "the second due receipt is owed"); + let snapshot = db.read_snapshot().await.unwrap(); + let mut rows = snapshot + .query( + "SELECT recovery_state FROM session_relation_receipts WHERE session_id = ?1", + params![poison_session], + ) + .await + .unwrap(); + assert_eq!( + rows.next() + .await + .unwrap() + .unwrap() + .get::(0) + .unwrap(), + "permanent", + "the earlier due receipt must settle on the first page" + ); + drop(rows); + drop(snapshot); + + let recovered_second = SessionTemporalAccess::new(&*db) + .recover_pending_session_relation_projection_page( + 1, + std::sync::Arc::new(tracedecay_graph_db::NeverCancelled), + ) + .await + .unwrap(); + assert_eq!(recovered_second.processed, 1); + assert!(!recovered_second.has_more); + let idle = SessionTemporalAccess::new(&*db) + .recover_pending_session_relation_projection_page( + 1, + std::sync::Arc::new(tracedecay_graph_db::NeverCancelled), + ) + .await + .unwrap(); + assert_eq!(idle.processed, 0, "settled receipts stay parked"); + assert!(!idle.has_more); + assert!( + idle.next_retry_at_unix_seconds.is_some(), + "sleeping receipts must keep their future retry deadline" + ); + // Waking the entire deferred backlog switches to the creation-ordered + // catch-up path. A one-item page must still surface a sentinel and settle + // the oldest durable receipt, even when its journal is unavailable. + let transaction = db.begin_write_transaction().await.unwrap(); + let woken = transaction + .execute( + "UPDATE session_relation_receipts + SET recovery_next_attempt_at = 0 + WHERE session_id LIKE 'deferred-relation-%'", + (), + ) + .await + .unwrap(); + assert_eq!(woken, 256); + transaction.commit().await.unwrap(); + let dense = SessionTemporalAccess::new(&*db) + .recover_pending_session_relation_projection_page( + 1, + std::sync::Arc::new(tracedecay_graph_db::NeverCancelled), + ) + .await + .unwrap(); + assert_eq!(dense.processed, 1); + assert!(dense.has_more, "a dense due queue keeps the page sentinel"); + let snapshot = db.read_snapshot().await.unwrap(); + let mut rows = snapshot + .query( + "SELECT recovery_state, recovery_failure_code + FROM session_relation_receipts + WHERE session_id = 'deferred-relation-0001'", + (), + ) + .await + .unwrap(); + let row = rows.next().await.unwrap().unwrap(); + assert_eq!(row.get::(0).unwrap(), "permanent"); + assert_eq!( + row.get::>(1).unwrap().as_deref(), + Some("journal_missing") + ); } #[test] diff --git a/crates/tracedecay-session-temporal-store/src/relation_projection.rs b/crates/tracedecay-session-temporal-store/src/relation_projection.rs index 8b5058dcecd..33403ba3d3e 100644 --- a/crates/tracedecay-session-temporal-store/src/relation_projection.rs +++ b/crates/tracedecay-session-temporal-store/src/relation_projection.rs @@ -6,7 +6,7 @@ use tracedecay_domain::{ SessionProjectionGenerationV1, TemporalValidityV1, ThreadId, UtcMicros, }; use tracedecay_graph_db::{GraphCancellation, GraphWatermark}; -use tracedecay_runtime_core::db::engine::params; +use tracedecay_runtime_core::db::engine::{IntoParams, params}; use tracedecay_store::{SessionStoreError, SessionStoreResult}; use super::operations::CanonicalPublicationManifest; @@ -17,7 +17,7 @@ use super::relations::{ ThreadHierarchyRelation, WorkflowAgentMembership, }; use crate::handle::{ - SessionTemporalAccess, SessionTemporalExec, SessionTemporalRegisteredDb, + SessionTemporalAccess, SessionTemporalExec, SessionTemporalQuery, SessionTemporalRegisteredDb, SessionTemporalWriteTxn, }; @@ -28,6 +28,25 @@ const DEFAULT_MAX_RELATIONS: usize = 100_000; // reconstruction and its receipt acknowledgement; each retry re-reads the // refreshed generation, so the bound only limits back-to-back supersessions. const APPLY_PUBLICATION_RACE_ATTEMPTS: usize = 3; +const INITIAL_RECOVERY_PROBE_ROWS: i64 = 64; + +// Seek the next distinct deadline in each eligible state. A fifth live +// deadline means the four equality ranges for that state are insufficient. +const RECOVERY_DEADLINES_SQL: &str = " + WITH RECURSIVE states(kind) AS (VALUES('pending'), ('retryable')), + bands(kind, deadline, remaining) AS ( + SELECT kind, ( + SELECT MIN(recovery_next_attempt_at) FROM session_relation_receipts + WHERE state = 'pending' AND recovery_state = states.kind + AND recovery_next_attempt_at <= ?2 + ), 4 FROM states + UNION ALL + SELECT kind, ( + SELECT MIN(recovery_next_attempt_at) FROM session_relation_receipts + WHERE state = 'pending' AND recovery_state = bands.kind + AND recovery_next_attempt_at > bands.deadline AND recovery_next_attempt_at <= ?2 + ), remaining - 1 FROM bands WHERE deadline IS NOT NULL AND remaining > 0 + )"; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct SessionRelationRecoveryPage { @@ -297,31 +316,79 @@ impl SessionTemporalAccess<'_, D> { let (scope, _) = self .session_relation_store() .map_err(|error| storage(RECONSTRUCT_OPERATION, error))?; - let mut rows = snapshot + // An idle pass with only sleeping retries must not scan all pending + // receipts in creation order. The recovery-due index answers this + // probe without visiting each deferred journal row. + let mut due_rows = snapshot .query( - "SELECT receipt.session_id, receipt.generation, receipt.scope_kind, - receipt.scope_id, journal.projection_json, - receipt.recovery_failure_count - FROM session_relation_receipts AS receipt - LEFT JOIN session_relation_effect_journal AS journal - ON journal.session_id = receipt.session_id - AND journal.generation = receipt.generation - WHERE receipt.state = 'pending' - AND receipt.recovery_state IN ('pending', 'retryable') - AND receipt.recovery_next_attempt_at <= unixepoch() - ORDER BY receipt.created_at, receipt.session_id, receipt.generation - LIMIT ?1", - params![bounded_query_limit(limit)?], + "SELECT MIN(recovery_next_attempt_at), unixepoch() + FROM session_relation_receipts + WHERE state = 'pending' + AND recovery_state IN ('pending', 'retryable')", + (), ) .await .map_err(|error| storage(RECONSTRUCT_OPERATION, error))?; + let earliest = due_rows + .next() + .await + .map_err(|error| storage(RECONSTRUCT_OPERATION, error))? + .ok_or_else(|| { + storage_message( + RECONSTRUCT_OPERATION, + "relation retry probe returned no aggregate row", + ) + })?; + let retry_at: Option = earliest + .get(0) + .map_err(|error| storage(RECONSTRUCT_OPERATION, error))?; + let now: i64 = earliest + .get(1) + .map_err(|error| storage(RECONSTRUCT_OPERATION, error))?; + drop(due_rows); + if retry_at.is_none_or(|deadline| deadline > now) { + return Ok(SessionRelationRecoveryPage { + recovered: 0, + processed: 0, + has_more: false, + next_retry_at_unix_seconds: retry_at, + }); + } + let query_limit = bounded_query_limit(limit)?; + let candidates = + recovery_candidate_query(&snapshot, now, query_limit, &cancellation).await?; + // Drive hydration from the bounded page: a planner-selected receipt + // outer loop would revisit the sleeping backlog on each small page. + let candidate_sql = format!( + "WITH candidates AS MATERIALIZED ({candidates}), page AS MATERIALIZED ( + SELECT created_at, session_id, generation FROM candidates + ORDER BY created_at, session_id, generation LIMIT (?1 - 1) + ) + SELECT page.session_id, page.generation, receipt.scope_kind, receipt.scope_id, + journal.projection_json, receipt.recovery_failure_count, + (SELECT COUNT(*) FROM candidates) = ?1 + FROM page CROSS JOIN session_relation_receipts AS receipt + ON receipt.session_id = page.session_id AND receipt.generation = page.generation + LEFT JOIN session_relation_effect_journal AS journal + ON journal.session_id = page.session_id AND journal.generation = page.generation + ORDER BY page.created_at, page.session_id, page.generation" + ); + let mut rows = snapshot + .query(candidate_sql.as_str(), params![query_limit, now]) + .await + .map_err(|error| storage(RECONSTRUCT_OPERATION, error))?; let mut pending = Vec::new(); + let mut has_more = false; while let Some(row) = rows .next() .await .map_err(|error| storage(RECONSTRUCT_OPERATION, error))? { require_not_cancelled(&cancellation)?; + has_more = row + .get::(6) + .map_err(|error| storage(RECONSTRUCT_OPERATION, error))? + != 0; pending.push(PendingRelationRecovery { session_id: row .get(0) @@ -349,8 +416,6 @@ impl SessionTemporalAccess<'_, D> { SessionRelationScope::ProjectSessions { .. } => "project_sessions", SessionRelationScope::ProfileSessions { .. } => "profile_sessions", }; - let has_more = pending.len() > limit; - pending.truncate(limit); let mut recovered = 0_usize; let mut processed = 0_usize; for candidate in &pending { @@ -1600,6 +1665,100 @@ async fn active_generation( }) } +async fn recovery_probe( + snapshot: &impl SessionTemporalQuery, + sql: &str, + values: impl IntoParams + Send, +) -> SessionStoreResult { + let mut rows = snapshot + .query(sql, values) + .await + .map_err(|error| storage(RECONSTRUCT_OPERATION, error))?; + rows.next() + .await + .map_err(|error| storage(RECONSTRUCT_OPERATION, error))? + .ok_or_else(|| storage_message(RECONSTRUCT_OPERATION, "missing relation recovery probe"))? + .get::(0) + .map(|value| value != 0) + .map_err(|error| storage(RECONSTRUCT_OPERATION, error)) +} + +async fn recovery_candidate_query( + snapshot: &impl SessionTemporalQuery, + now: i64, + query_limit: i64, + cancellation: &Arc, +) -> SessionStoreResult { + let mut window = INITIAL_RECOVERY_PROBE_ROWS; + let index = loop { + require_not_cancelled(cancellation)?; + // Stop the creation scan as soon as the requested page and sentinel + // are present, even if the current probe window is much larger. + let page_in_prefix = "SELECT EXISTS ( + SELECT 1 FROM ( + SELECT recovery_state, recovery_next_attempt_at FROM session_relation_receipts + INDEXED BY idx_session_relation_receipts_pending + WHERE state = 'pending' + ORDER BY created_at, session_id, generation LIMIT ?1 + ) WHERE recovery_state IN ('pending', 'retryable') AND recovery_next_attempt_at <= ?2 + LIMIT 1 OFFSET (?3 - 1) + )"; + if recovery_probe(snapshot, page_in_prefix, params![window, now, query_limit]).await? { + break "idx_session_relation_receipts_pending"; + } + + if window == INITIAL_RECOVERY_PROBE_ROWS { + let complete_bands = format!( + "{RECOVERY_DEADLINES_SQL} SELECT NOT EXISTS ( + SELECT 1 FROM bands WHERE deadline IS NOT NULL AND remaining = 0)" + ); + if recovery_probe(snapshot, &complete_bands, params![query_limit, now]).await? { + // Each exact state/deadline range is already in creation order. + // Only its first page can contribute to the global first page. + return Ok(format!( + "{RECOVERY_DEADLINES_SQL} + SELECT r.created_at, r.session_id, r.generation FROM bands + CROSS JOIN session_relation_receipts AS r + WHERE bands.deadline IS NOT NULL AND r.rowid IN ( + SELECT rowid FROM session_relation_receipts + INDEXED BY idx_session_relation_receipts_recovery_due + WHERE state = 'pending' AND recovery_state = bands.kind + AND recovery_next_attempt_at = bands.deadline + ORDER BY created_at, session_id, generation LIMIT ?1 + ) + ORDER BY r.created_at, r.session_id, r.generation LIMIT ?1" + )); + } + } + + // For fragmented deadlines, compare the distance to the ordered page + // against the complete due population. Neither index is scanned past + // the growing window until it proves which bounded result to read. + if !recovery_probe( + snapshot, + "SELECT EXISTS ( + SELECT 1 FROM session_relation_receipts + INDEXED BY idx_session_relation_receipts_recovery_due + WHERE state = 'pending' AND recovery_state IN ('pending', 'retryable') + AND recovery_next_attempt_at <= ?2 LIMIT 1 OFFSET ?1 + )", + // The covering deadline index can inspect four keys for roughly + // the work of fetching one receipt through the creation index. + params![window.saturating_mul(4), now], + ) + .await? + { + break "idx_session_relation_receipts_recovery_due"; + } + window = window.saturating_mul(2); + }; + Ok(format!( + "SELECT created_at, session_id, generation FROM session_relation_receipts INDEXED BY {index} + WHERE state = 'pending' AND recovery_state IN ('pending', 'retryable') + AND recovery_next_attempt_at <= ?2 ORDER BY created_at, session_id, generation LIMIT ?1" + )) +} + fn bounded_query_limit(limit: usize) -> SessionStoreResult { let bounded = limit .checked_add(1) diff --git a/crates/tracedecay/tests/dashboard_api_test/dashboard_api_support.rs b/crates/tracedecay/tests/dashboard_api_test/dashboard_api_support.rs index 35a94248875..22402def73a 100644 --- a/crates/tracedecay/tests/dashboard_api_test/dashboard_api_support.rs +++ b/crates/tracedecay/tests/dashboard_api_test/dashboard_api_support.rs @@ -1105,10 +1105,6 @@ pub(crate) async fn start_dashboard_configuration_fixture() -> DashboardFixture start_dashboard_fixture_with_options(false, false, true).await } -pub(crate) async fn start_dashboard_retained_memory_fixture() -> DashboardFixture { - start_dashboard_fixture_with_options(false, true, true).await -} - /// Starts a dashboard fixture with a fake `DashboardDeliveryReadPortV1` and /// code-index freshness reader mounted, proving the admitted-PR HTTP path /// (`GET /api/delivery/inbox`) joins the provider read with an indexed head diff --git a/crates/tracedecay/tests/dashboard_api_test/memory_curation.rs b/crates/tracedecay/tests/dashboard_api_test/memory_curation.rs index 878afd77d04..fa9b596d1de 100644 --- a/crates/tracedecay/tests/dashboard_api_test/memory_curation.rs +++ b/crates/tracedecay/tests/dashboard_api_test/memory_curation.rs @@ -57,7 +57,7 @@ fn retained_effect_payload<'a>(response: &'a Value, operation: &str) -> &'a Valu fn retained_admin_journey_commits_add_update_feedback_and_remove() { let runtime = create_runtime(); runtime.block_on(async { - let fixture = start_dashboard_retained_memory_fixture().await; + let fixture = start_dashboard_configuration_fixture().await; let agent = http_agent(); let (status, added) = post_json_body( @@ -214,7 +214,7 @@ fn retained_admin_journey_commits_add_update_feedback_and_remove() { fn fact_detail_keeps_a_superseded_fact_readable_by_id() { let runtime = create_runtime(); runtime.block_on(async { - let fixture = start_dashboard_retained_memory_fixture().await; + let fixture = start_dashboard_configuration_fixture().await; let agent = http_agent(); let add = |content: &str| { let (status, added) = post_json_body( @@ -276,7 +276,7 @@ fn fact_detail_keeps_a_superseded_fact_readable_by_id() { fn retained_mutations_deny_foreign_project_scope_without_a_receipt() { let runtime = create_runtime(); runtime.block_on(async { - let fixture = start_dashboard_retained_memory_fixture().await; + let fixture = start_dashboard_configuration_fixture().await; let agent = http_agent(); let (_target_root, target_cg) = crate::projects::setup_target_project(&fixture).await; let target_project_id = target_cg