1use turnframe_core::ids::{CaseRevision, ConversationId, InteractionId, OptionId, TurnId};
4use turnframe_core::interaction::InteractionStatus;
5
6use super::ConformanceFailure;
7use super::fixtures::{
8 account, at, card, card_spec, case, case_key, ensure, ensure_eq, ensure_error, ensure_ok,
9 epoch, journal_entry, other_account, other_case, ttl, user_turn,
10};
11use crate::conversation::ConversationRecord;
12use crate::error::StoreError;
13use crate::interaction::{InvalidationReason, ResolutionOutcome};
14use crate::stores::Stores;
15
16pub async fn check_blocking_interaction_conflict(
21 stores: &Stores,
22) -> Result<(), ConformanceFailure> {
23 const CHECK: &str = "check_blocking_interaction_conflict";
24 let interactions = stores.interactions();
25 let account = account();
26 let conversation = ConversationId::new();
27 let turn = TurnId::new();
28
29 let first = card(
30 CHECK,
31 card_spec("first", case(1)),
32 InteractionId::new(),
33 account.clone(),
34 conversation,
35 turn,
36 epoch(),
37 )?;
38 ensure_ok(
39 CHECK,
40 "inserting the first blocking card",
41 interactions.insert(first.clone()).await,
42 )?;
43
44 let second = card(
45 CHECK,
46 card_spec("second", case(1)),
47 InteractionId::new(),
48 account.clone(),
49 conversation,
50 turn,
51 at(1),
52 )?;
53 ensure_error(
54 CHECK,
55 "a second blocking card on the same case",
56 interactions.insert(second).await,
57 &StoreError::Conflict,
58 )?;
59
60 let non_blocking = card(
61 CHECK,
62 card_spec("aside", case(1)).non_blocking(),
63 InteractionId::new(),
64 account.clone(),
65 conversation,
66 turn,
67 at(2),
68 )?;
69 ensure_ok(
70 CHECK,
71 "a non-blocking card on the same case does not take the slot",
72 interactions.insert(non_blocking).await,
73 )?;
74
75 let other = card(
76 CHECK,
77 card_spec("other-case", other_case(1)),
78 InteractionId::new(),
79 account.clone(),
80 conversation,
81 turn,
82 at(3),
83 )?;
84 ensure_ok(
85 CHECK,
86 "a blocking card on another case",
87 interactions.insert(other).await,
88 )?;
89
90 let elsewhere = card(
91 CHECK,
92 card_spec("other-tenant", case(1)),
93 InteractionId::new(),
94 other_account(),
95 conversation,
96 turn,
97 at(4),
98 )?;
99 ensure_ok(
100 CHECK,
101 "the slot is per account, so another tenant may hold its own",
102 interactions.insert(elsewhere).await,
103 )?;
104
105 ensure_error(
106 CHECK,
107 "re-inserting the same identifier",
108 interactions.insert(first.clone()).await,
109 &StoreError::Conflict,
110 )?;
111
112 let open = ensure_ok(
113 CHECK,
114 "listing the open cards of the case",
115 interactions.list_open_for_case(&account, &case_key()).await,
116 )?;
117 ensure_eq(CHECK, "open cards on the case", &open.len(), &2)?;
118 ensure_eq(CHECK, "the first open card", &open[0].id, &first.id)
119}
120
121pub async fn check_blocking_interaction_replace(stores: &Stores) -> Result<(), ConformanceFailure> {
124 const CHECK: &str = "check_blocking_interaction_replace";
125 let interactions = stores.interactions();
126 let account = account();
127 let conversation = ConversationId::new();
128 let turn = TurnId::new();
129
130 let first = card(
131 CHECK,
132 card_spec("first", case(1)),
133 InteractionId::new(),
134 account.clone(),
135 conversation,
136 turn,
137 epoch(),
138 )?;
139 ensure_ok(
140 CHECK,
141 "inserting the occupant",
142 interactions.insert(first.clone()).await,
143 )?;
144
145 let replacement = card(
146 CHECK,
147 card_spec("replacement", case(1)),
148 InteractionId::new(),
149 account.clone(),
150 conversation,
151 turn,
152 at(1),
153 )?;
154 let invalidated = ensure_ok(
155 CHECK,
156 "replacing the occupant",
157 interactions
158 .insert_replacing_blocking(replacement.clone())
159 .await,
160 )?;
161 ensure_eq(
162 CHECK,
163 "invalidated occupants",
164 &invalidated,
165 &vec![first.id],
166 )?;
167 ensure(
168 CHECK,
169 replacement.id != first.id,
170 "the replacement must carry a new identifier",
171 )?;
172
173 let old = ensure_ok(
174 CHECK,
175 "reading the replaced card",
176 interactions.get(&account, &first.id).await,
177 )?;
178 ensure_eq(
179 CHECK,
180 "status of the replaced card",
181 &old.status(),
182 &InteractionStatus::Invalidated,
183 )?;
184 ensure_eq(
185 CHECK,
186 "reason recorded on the replaced card",
187 &old.invalidation.map(|record| record.reason),
188 &Some(InvalidationReason::Superseded { by: replacement.id }),
189 )?;
190
191 let current = ensure_ok(
192 CHECK,
193 "reading the replacement",
194 interactions.get(&account, &replacement.id).await,
195 )?;
196 ensure_eq(
197 CHECK,
198 "status of the replacement",
199 ¤t.status(),
200 &InteractionStatus::Active,
201 )?;
202
203 ensure_ok(
205 CHECK,
206 "starting resolution of the replacement",
207 interactions
208 .begin_resolution(
209 &account,
210 &replacement.id,
211 InteractionStatus::Active,
212 OptionId::from("ack"),
213 turn,
214 )
215 .await,
216 )?;
217 let third = card(
218 CHECK,
219 card_spec("third", case(1)),
220 InteractionId::new(),
221 account.clone(),
222 conversation,
223 turn,
224 at(2),
225 )?;
226 ensure_error(
227 CHECK,
228 "replacing a card that is already resolving",
229 interactions.insert_replacing_blocking(third).await,
230 &StoreError::Conflict,
231 )
232}
233
234pub async fn check_cross_tenant_isolation(stores: &Stores) -> Result<(), ConformanceFailure> {
237 const CHECK: &str = "check_cross_tenant_isolation";
238 let account = account();
239 let intruder = other_account();
240 let conversation = ConversationId::new();
241 let turn = TurnId::new();
242
243 ensure_ok(
244 CHECK,
245 "creating the conversation",
246 stores
247 .conversations()
248 .create_conversation(ConversationRecord::new(
249 conversation,
250 account.clone(),
251 epoch(),
252 ))
253 .await,
254 )?;
255 ensure_ok(
256 CHECK,
257 "appending the user turn",
258 stores
259 .conversations()
260 .append_user_turn(user_turn(&account, conversation, turn, epoch()))
261 .await,
262 )?;
263 let interaction = card(
264 CHECK,
265 card_spec("private", case(1)),
266 InteractionId::new(),
267 account.clone(),
268 conversation,
269 turn,
270 epoch(),
271 )?;
272 ensure_ok(
273 CHECK,
274 "inserting the card",
275 stores.interactions().insert(interaction.clone()).await,
276 )?;
277 let command = turnframe_core::ids::CommandId::new();
278 ensure_ok(
279 CHECK,
280 "admitting the command",
281 stores
282 .journal()
283 .begin(journal_entry(&account, turn, command, "isolation", epoch()))
284 .await,
285 )?;
286 ensure_ok(
287 CHECK,
288 "writing the replay record",
289 stores
290 .replay()
291 .put(turnframe_core::replay::ReplayRecord::received(
292 turn,
293 conversation,
294 account.clone(),
295 epoch(),
296 ))
297 .await,
298 )?;
299
300 ensure_error(
301 CHECK,
302 "loading another tenant's conversation",
303 stores
304 .conversations()
305 .load_conversation(&intruder, &conversation)
306 .await,
307 &StoreError::NotFound,
308 )?;
309 ensure_error(
310 CHECK,
311 "loading another tenant's turn",
312 stores.conversations().load_turn(&intruder, &turn).await,
313 &StoreError::NotFound,
314 )?;
315 ensure_error(
316 CHECK,
317 "reading another tenant's phase marker",
318 stores.conversations().turn_phase(&intruder, &turn).await,
319 &StoreError::NotFound,
320 )?;
321 ensure_error(
322 CHECK,
323 "reading another tenant's card",
324 stores.interactions().get(&intruder, &interaction.id).await,
325 &StoreError::NotFound,
326 )?;
327 ensure_error(
328 CHECK,
329 "resolving another tenant's card",
330 stores
331 .interactions()
332 .begin_resolution(
333 &intruder,
334 &interaction.id,
335 InteractionStatus::Active,
336 OptionId::from("ack"),
337 turn,
338 )
339 .await,
340 &StoreError::NotFound,
341 )?;
342 ensure_error(
343 CHECK,
344 "settling another tenant's card",
345 stores
346 .interactions()
347 .finish_resolution(
348 &intruder,
349 &interaction.id,
350 ResolutionOutcome::Failed {
351 code: "nope".to_owned(),
352 },
353 )
354 .await,
355 &StoreError::NotFound,
356 )?;
357 ensure_error(
358 CHECK,
359 "reading another tenant's journal entry",
360 stores.journal().get(&intruder, &command).await,
361 &StoreError::NotFound,
362 )?;
363 ensure_error(
364 CHECK,
365 "reading another tenant's replay record",
366 stores.replay().get(&intruder, &turn).await,
367 &StoreError::NotFound,
368 )?;
369
370 let listed = ensure_ok(
371 CHECK,
372 "listing another tenant's open cards",
373 stores
374 .interactions()
375 .list_open_for_conversation(&intruder, &conversation)
376 .await,
377 )?;
378 ensure(
379 CHECK,
380 listed.is_empty(),
381 "another tenant must see no cards of this conversation",
382 )?;
383 let entries = ensure_ok(
384 CHECK,
385 "listing another tenant's journal entries",
386 stores.journal().for_turn(&intruder, &turn).await,
387 )?;
388 ensure(
389 CHECK,
390 entries.is_empty(),
391 "another tenant must see no journal entries of this turn",
392 )?;
393
394 ensure_error(
396 CHECK,
397 "reading an identifier that never existed",
398 stores
399 .interactions()
400 .get(&account, &InteractionId::new())
401 .await,
402 &StoreError::NotFound,
403 )
404}
405
406pub async fn check_begin_resolution_cas(stores: &Stores) -> Result<(), ConformanceFailure> {
408 const CHECK: &str = "check_begin_resolution_cas";
409 let interactions = stores.interactions();
410 let account = account();
411 let conversation = ConversationId::new();
412 let turn = TurnId::new();
413 let interaction = card(
414 CHECK,
415 card_spec("cas", case(1)),
416 InteractionId::new(),
417 account.clone(),
418 conversation,
419 turn,
420 epoch(),
421 )?;
422 ensure_ok(
423 CHECK,
424 "inserting the card",
425 interactions.insert(interaction.clone()).await,
426 )?;
427
428 ensure_error(
429 CHECK,
430 "expecting Resolving on an Active card",
431 interactions
432 .begin_resolution(
433 &account,
434 &interaction.id,
435 InteractionStatus::Resolving,
436 OptionId::from("ack"),
437 turn,
438 )
439 .await,
440 &StoreError::Conflict,
441 )?;
442 ensure_error(
443 CHECK,
444 "resolving an identifier that does not exist",
445 interactions
446 .begin_resolution(
447 &account,
448 &InteractionId::new(),
449 InteractionStatus::Active,
450 OptionId::from("ack"),
451 turn,
452 )
453 .await,
454 &StoreError::NotFound,
455 )?;
456
457 let record = ensure_ok(
458 CHECK,
459 "starting resolution from Active",
460 interactions
461 .begin_resolution(
462 &account,
463 &interaction.id,
464 InteractionStatus::Active,
465 OptionId::from("ack"),
466 turn,
467 )
468 .await,
469 )?;
470 ensure_eq(
471 CHECK,
472 "status after begin_resolution",
473 &record.status(),
474 &InteractionStatus::Resolving,
475 )?;
476 ensure_eq(
477 CHECK,
478 "option recorded on the card",
479 &record.interaction.resolved_option_id,
480 &Some(OptionId::from("ack")),
481 )?;
482 ensure_eq(
483 CHECK,
484 "turn recorded on the card",
485 &record.resolved_by_turn,
486 &Some(turn),
487 )?;
488
489 ensure_error(
491 CHECK,
492 "a second begin_resolution expecting Active",
493 interactions
494 .begin_resolution(
495 &account,
496 &interaction.id,
497 InteractionStatus::Active,
498 OptionId::from("ack"),
499 turn,
500 )
501 .await,
502 &StoreError::Conflict,
503 )?;
504 ensure_error(
505 CHECK,
506 "beginning resolution from Resolving",
507 interactions
508 .begin_resolution(
509 &account,
510 &interaction.id,
511 InteractionStatus::Resolving,
512 OptionId::from("ack"),
513 turn,
514 )
515 .await,
516 &StoreError::Conflict,
517 )
518}
519
520pub async fn check_finish_resolution_idempotent(stores: &Stores) -> Result<(), ConformanceFailure> {
522 const CHECK: &str = "check_finish_resolution_idempotent";
523 let interactions = stores.interactions();
524 let account = account();
525 let conversation = ConversationId::new();
526 let turn = TurnId::new();
527 let events = vec![turnframe_core::ids::EventId::new()];
528
529 let resolved = card(
530 CHECK,
531 card_spec("resolved", case(1)),
532 InteractionId::new(),
533 account.clone(),
534 conversation,
535 turn,
536 epoch(),
537 )?;
538 ensure_ok(
539 CHECK,
540 "inserting the card",
541 interactions.insert(resolved.clone()).await,
542 )?;
543 ensure_ok(
544 CHECK,
545 "starting resolution",
546 interactions
547 .begin_resolution(
548 &account,
549 &resolved.id,
550 InteractionStatus::Active,
551 OptionId::from("ack"),
552 turn,
553 )
554 .await,
555 )?;
556 let settled = ensure_ok(
557 CHECK,
558 "settling the card as Resolved",
559 interactions
560 .finish_resolution(
561 &account,
562 &resolved.id,
563 ResolutionOutcome::Resolved {
564 event_ids: events.clone(),
565 },
566 )
567 .await,
568 )?;
569 ensure_eq(
570 CHECK,
571 "status after Resolved",
572 &settled.status(),
573 &InteractionStatus::Resolved,
574 )?;
575 ensure_eq(
576 CHECK,
577 "events backing the resolution",
578 &settled.resolution_event_ids,
579 &events,
580 )?;
581
582 let repeat = ensure_ok(
583 CHECK,
584 "settling the same way again",
585 interactions
586 .finish_resolution(
587 &account,
588 &resolved.id,
589 ResolutionOutcome::Resolved {
590 event_ids: events.clone(),
591 },
592 )
593 .await,
594 )?;
595 ensure_eq(
596 CHECK,
597 "the repeated finish must change nothing",
598 &repeat,
599 &settled,
600 )?;
601 ensure_error(
602 CHECK,
603 "settling as Resolved with different events",
604 interactions
605 .finish_resolution(
606 &account,
607 &resolved.id,
608 ResolutionOutcome::Resolved {
609 event_ids: vec![turnframe_core::ids::EventId::new()],
610 },
611 )
612 .await,
613 &StoreError::Conflict,
614 )?;
615 ensure_error(
616 CHECK,
617 "settling a resolved card as Failed",
618 interactions
619 .finish_resolution(
620 &account,
621 &resolved.id,
622 ResolutionOutcome::Failed {
623 code: "late".to_owned(),
624 },
625 )
626 .await,
627 &StoreError::Conflict,
628 )?;
629
630 let restored = card(
632 CHECK,
633 card_spec("restored", other_case(1)),
634 InteractionId::new(),
635 account.clone(),
636 conversation,
637 turn,
638 at(1),
639 )?;
640 ensure_ok(
641 CHECK,
642 "inserting the card to restore",
643 interactions.insert(restored.clone()).await,
644 )?;
645 ensure_ok(
646 CHECK,
647 "starting resolution of the card to restore",
648 interactions
649 .begin_resolution(
650 &account,
651 &restored.id,
652 InteractionStatus::Active,
653 OptionId::from("ack"),
654 turn,
655 )
656 .await,
657 )?;
658 let back = ensure_ok(
659 CHECK,
660 "restoring the card",
661 interactions
662 .finish_resolution(&account, &restored.id, ResolutionOutcome::RestoreActive)
663 .await,
664 )?;
665 ensure_eq(
666 CHECK,
667 "status after RestoreActive",
668 &back.status(),
669 &InteractionStatus::Active,
670 )?;
671 ensure_eq(
672 CHECK,
673 "the chosen option is cleared so the card is answerable again",
674 &back.interaction.resolved_option_id,
675 &None,
676 )?;
677 ensure_ok(
678 CHECK,
679 "restoring an already restored card",
680 interactions
681 .finish_resolution(&account, &restored.id, ResolutionOutcome::RestoreActive)
682 .await,
683 )
684 .map(|_| ())
685}
686
687pub async fn check_revision_invalidation_respects_independence(
690 stores: &Stores,
691) -> Result<(), ConformanceFailure> {
692 const CHECK: &str = "check_revision_invalidation_respects_independence";
693 let interactions = stores.interactions();
694 let account = account();
695 let conversation = ConversationId::new();
696 let turn = TurnId::new();
697
698 let bound = card(
699 CHECK,
700 card_spec("bound", case(1)),
701 InteractionId::new(),
702 account.clone(),
703 conversation,
704 turn,
705 epoch(),
706 )?;
707 let independent = card(
708 CHECK,
709 card_spec("independent", case(1))
710 .non_blocking()
711 .revision_independent(),
712 InteractionId::new(),
713 account.clone(),
714 conversation,
715 turn,
716 at(1),
717 )?;
718 let current = card(
719 CHECK,
720 card_spec("current", case(2)).non_blocking(),
721 InteractionId::new(),
722 account.clone(),
723 conversation,
724 turn,
725 at(2),
726 )?;
727 let elsewhere = card(
728 CHECK,
729 card_spec("elsewhere", other_case(1)),
730 InteractionId::new(),
731 account.clone(),
732 conversation,
733 turn,
734 at(3),
735 )?;
736 for (what, interaction) in [
737 ("bound", &bound),
738 ("independent", &independent),
739 ("current", ¤t),
740 ("elsewhere", &elsewhere),
741 ] {
742 ensure_ok(
743 CHECK,
744 &format!("inserting the {what} card"),
745 interactions.insert(interaction.clone()).await,
746 )?;
747 }
748
749 let invalidated = ensure_ok(
750 CHECK,
751 "invalidating the case at revision 2",
752 interactions
753 .invalidate_for_case(
754 &account,
755 &case_key(),
756 turnframe_core::ids::CaseRevision(2),
757 InvalidationReason::RevisionChanged,
758 )
759 .await,
760 )?;
761 ensure_eq(
762 CHECK,
763 "only the card bound to the old revision is invalidated",
764 &invalidated,
765 &vec![bound.id],
766 )?;
767
768 let stale = ensure_ok(
769 CHECK,
770 "reading the invalidated card",
771 interactions.get(&account, &bound.id).await,
772 )?;
773 ensure_eq(
774 CHECK,
775 "status of the invalidated card",
776 &stale.status(),
777 &InteractionStatus::Invalidated,
778 )?;
779 ensure_eq(
780 CHECK,
781 "reason recorded on the invalidated card",
782 &stale.invalidation.as_ref().map(|record| &record.reason),
783 &Some(&InvalidationReason::RevisionChanged),
784 )?;
785 ensure_eq(
786 CHECK,
787 "the revision that made the card stale",
788 &stale.invalidation.and_then(|record| record.new_revision),
789 &Some(turnframe_core::ids::CaseRevision(2)),
790 )?;
791
792 for (what, id) in [
793 ("revision-independent", independent.id),
794 ("already current", current.id),
795 ("on another case", elsewhere.id),
796 ] {
797 let survivor = ensure_ok(
798 CHECK,
799 &format!("reading the {what} card"),
800 interactions.get(&account, &id).await,
801 )?;
802 ensure_eq(
803 CHECK,
804 &format!("status of the {what} card"),
805 &survivor.status(),
806 &InteractionStatus::Active,
807 )?;
808 }
809 Ok(())
810}
811
812pub async fn check_answered_blocking_card_is_remembered(
821 stores: &Stores,
822) -> Result<(), ConformanceFailure> {
823 const CHECK: &str = "check_answered_blocking_card_is_remembered";
824 let interactions = stores.interactions();
825 let account = account();
826 let conversation = ConversationId::new();
827 let turn = TurnId::new();
828 let key = case(1).key();
829
830 ensure_eq(
831 CHECK,
832 "nothing has been answered before anything exists",
833 &ensure_ok(
834 CHECK,
835 "asking about a case with no cards",
836 interactions
837 .blocking_answered_at(&account, &key, CaseRevision(1))
838 .await,
839 )?,
840 &false,
841 )?;
842
843 let interaction = card(
844 CHECK,
845 card_spec("send_confirmation", case(1)),
846 InteractionId::new(),
847 account.clone(),
848 conversation,
849 turn,
850 epoch(),
851 )?;
852 ensure_ok(
853 CHECK,
854 "inserting the card",
855 interactions.insert(interaction.clone()).await,
856 )?;
857 ensure_eq(
858 CHECK,
859 "a card on screen has not been answered",
860 &ensure_ok(
861 CHECK,
862 "asking about an active card",
863 interactions
864 .blocking_answered_at(&account, &key, CaseRevision(1))
865 .await,
866 )?,
867 &false,
868 )?;
869
870 ensure_ok(
871 CHECK,
872 "beginning the resolution",
873 interactions
874 .begin_resolution(
875 &account,
876 &interaction.id,
877 InteractionStatus::Active,
878 OptionId::from("not_now"),
879 turn,
880 )
881 .await,
882 )?;
883 ensure_ok(
884 CHECK,
885 "settling it with nothing committed, which is what declining is",
886 interactions
887 .finish_resolution(
888 &account,
889 &interaction.id,
890 ResolutionOutcome::Resolved {
891 event_ids: Vec::new(),
892 },
893 )
894 .await,
895 )?;
896
897 ensure_eq(
898 CHECK,
899 "the answer is remembered at the revision it was given at",
900 &ensure_ok(
901 CHECK,
902 "asking after the answer",
903 interactions
904 .blocking_answered_at(&account, &key, CaseRevision(1))
905 .await,
906 )?,
907 &true,
908 )?;
909 ensure_eq(
910 CHECK,
911 "and the question is open again once the case has moved",
912 &ensure_ok(
913 CHECK,
914 "asking at the next revision",
915 interactions
916 .blocking_answered_at(&account, &key, CaseRevision(2))
917 .await,
918 )?,
919 &false,
920 )?;
921 Ok(())
922}
923
924pub async fn check_interaction_expiry(stores: &Stores) -> Result<(), ConformanceFailure> {
926 const CHECK: &str = "check_interaction_expiry";
927 let interactions = stores.interactions();
928 let account = account();
929 let conversation = ConversationId::new();
930 let turn = TurnId::new();
931
932 let expiring = card(
933 CHECK,
934 card_spec("expiring", case(1)).expires_in(ttl(60)),
935 InteractionId::new(),
936 account.clone(),
937 conversation,
938 turn,
939 epoch(),
940 )?;
941 let permanent = card(
942 CHECK,
943 card_spec("permanent", case(1)).non_blocking(),
944 InteractionId::new(),
945 account.clone(),
946 conversation,
947 turn,
948 epoch(),
949 )?;
950 ensure_ok(
951 CHECK,
952 "inserting the expiring card",
953 interactions.insert(expiring.clone()).await,
954 )?;
955 ensure_ok(
956 CHECK,
957 "inserting the card without a deadline",
958 interactions.insert(permanent.clone()).await,
959 )?;
960
961 let early = ensure_ok(
962 CHECK,
963 "sweeping before the deadline",
964 interactions.expire_due(at(30)).await,
965 )?;
966 ensure(
967 CHECK,
968 early.is_empty(),
969 "a card must not expire before its deadline",
970 )?;
971
972 let due = ensure_ok(
973 CHECK,
974 "sweeping at the deadline",
975 interactions.expire_due(at(60)).await,
976 )?;
977 ensure_eq(
978 CHECK,
979 "cards expired by the sweep",
980 &due,
981 &vec![expiring.id],
982 )?;
983 let expired = ensure_ok(
984 CHECK,
985 "reading the expired card",
986 interactions.get(&account, &expiring.id).await,
987 )?;
988 ensure_eq(
989 CHECK,
990 "status after expiry",
991 &expired.status(),
992 &InteractionStatus::Expired,
993 )?;
994 let kept = ensure_ok(
995 CHECK,
996 "reading the card without a deadline",
997 interactions.get(&account, &permanent.id).await,
998 )?;
999 ensure_eq(
1000 CHECK,
1001 "a card without a deadline is untouched",
1002 &kept.status(),
1003 &InteractionStatus::Active,
1004 )?;
1005
1006 let again = ensure_ok(
1007 CHECK,
1008 "sweeping a second time",
1009 interactions.expire_due(at(600)).await,
1010 )?;
1011 ensure(
1012 CHECK,
1013 again.is_empty(),
1014 "a card must expire once, not on every sweep",
1015 )
1016}
1017
1018pub async fn check_administrative_invalidation_ignores_the_revision(
1032 stores: &Stores,
1033) -> Result<(), ConformanceFailure> {
1034 const CHECK: &str = "check_administrative_invalidation_ignores_the_revision";
1035 let interactions = stores.interactions();
1036 let account = account();
1037 let case_ref = case(4);
1038 let conversation = ConversationId::new();
1039
1040 let plain_id = InteractionId::new();
1046 let independent_id = InteractionId::new();
1047 let plain = card(
1048 CHECK,
1049 card_spec("administrative.plain", case_ref.clone()),
1050 plain_id,
1051 account.clone(),
1052 conversation,
1053 TurnId::new(),
1054 epoch(),
1055 )?;
1056 let independent = card(
1057 CHECK,
1058 card_spec("administrative.independent", case_ref.clone())
1059 .non_blocking()
1060 .revision_independent(),
1061 independent_id,
1062 account.clone(),
1063 conversation,
1064 TurnId::new(),
1065 at(1),
1066 )?;
1067 ensure_ok(
1068 CHECK,
1069 "writing a card at the current revision",
1070 interactions.insert(plain).await,
1071 )?;
1072 ensure_ok(
1073 CHECK,
1074 "writing a revision-independent card",
1075 interactions.insert(independent).await,
1076 )?;
1077
1078 let by_revision = ensure_ok(
1079 CHECK,
1080 "sweeping by revision at the revision the cards are bound to",
1081 interactions
1082 .invalidate_for_case(
1083 &account,
1084 &case_ref.key(),
1085 case_ref.expected_revision,
1086 InvalidationReason::RevisionChanged,
1087 )
1088 .await,
1089 )?;
1090 ensure(
1091 CHECK,
1092 by_revision.is_empty(),
1093 "the revision-driven sweep finds nothing while the case has not moved",
1094 )?;
1095
1096 let withdrawn = ensure_ok(
1097 CHECK,
1098 "withdrawing the case's cards administratively",
1099 interactions
1100 .invalidate_case_cards(
1101 &account,
1102 &case_ref.key(),
1103 InvalidationReason::Administrative {
1104 code: String::from("conformance.withdrawn"),
1105 },
1106 )
1107 .await,
1108 )?;
1109 ensure(
1110 CHECK,
1111 withdrawn.contains(&plain_id) && withdrawn.contains(&independent_id),
1112 "both cards are withdrawn, the revision-independent one included",
1113 )?;
1114
1115 for id in [plain_id, independent_id] {
1116 let record = ensure_ok(
1117 CHECK,
1118 "reading a withdrawn card",
1119 interactions.get(&account, &id).await,
1120 )?;
1121 ensure_eq(
1122 CHECK,
1123 "status after an administrative withdrawal",
1124 &record.interaction.status,
1125 &InteractionStatus::Invalidated,
1126 )?;
1127 }
1128
1129 Ok(())
1130}