Make an aggregate's own events a first-class mode
An aggregate with no default_sink already left events created in it alone, but treated doing so as a failure: it reported a skip per event per cycle saying no sink was configured, as though something had gone wrong. Nothing had. An aggregate is also a calendar, and holding events of its own is a legitimate way to use one. Skipped::NoSink is replaced by a kept_local count, reported plainly. `@local` joins the routing markers, so the mode also works per-event where a default_sink is configured — which was not previously expressible. Unlike every other marker it is deliberately not stripped: the others have done their job once the event reaches its source, whereas this one never leaves, so it has to stay legible for the next cycle to reach the same decision. `local` is therefore a reserved endpoint id, and configuring one is refused. This also fixes a real defect. `retarget` rebuilds the new target from the recorded links, which cover derived events only, so an event belonging to the aggregate itself did not follow the move — it stayed on the calendar being left behind while everything around it moved on, quietly. It is now carried across, since there is nothing to re-derive it from, and the new target's scheduling rule is applied on the way: this is a write to an aggregate like any other, and a guest list carried live onto a server that schedules would mail everyone on it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
7607078394
commit
47ad8b4c47
9 changed files with 298 additions and 26 deletions
|
|
@ -29,6 +29,12 @@ const HAND_WRITTEN: &str = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//phone//E
|
|||
DTSTART:20260911T140000Z\r\nDTEND:20260911T150000Z\r\nSUMMARY:Dentist\r\n\
|
||||
END:VEVENT\r\nEND:VCALENDAR\r\n";
|
||||
|
||||
/// The same event, asking to be left where it was written.
|
||||
const KEPT_LOCAL: &str = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//phone//EN\r\n\
|
||||
BEGIN:VEVENT\r\nUID:kept@phone\r\nDTSTAMP:20260101T000000Z\r\n\
|
||||
DTSTART:20260912T140000Z\r\nDTEND:20260912T150000Z\r\nSUMMARY:Haircut\r\n\
|
||||
DESCRIPTION:Around the corner\\n\\n@local\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n";
|
||||
|
||||
const HOLIDAY_FEED: &str = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//feed//EN\r\n\
|
||||
BEGIN:VEVENT\r\nUID:newyear@feed\r\nDTSTAMP:20260101T000000Z\r\n\
|
||||
DTSTART;VALUE=DATE:20260101\r\nDTEND;VALUE=DATE:20260102\r\n\
|
||||
|
|
@ -329,3 +335,51 @@ fn an_event_created_in_a_sinkless_aggregate_stays_where_it_is() {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `@local` opts one event out of a default sink that would otherwise take it.
|
||||
///
|
||||
/// The marker has to survive, unlike every other one: it is stripped markers
|
||||
/// that have done their job on arrival, whereas this event never leaves, so the
|
||||
/// next cycle has to be able to reach the same decision.
|
||||
#[test]
|
||||
fn the_local_marker_keeps_an_event_out_of_a_configured_sink() {
|
||||
let Some(fixture) = fixture() else { return };
|
||||
|
||||
assert!(fixture.calcalist.run(&["sync"]).succeeded());
|
||||
fixture
|
||||
.server
|
||||
.put_event("unified", "haircut.ics", KEPT_LOCAL);
|
||||
|
||||
for cycle in 1..=3 {
|
||||
let run = fixture.calcalist.run(&["sync"]);
|
||||
assert!(
|
||||
run.succeeded(),
|
||||
"cycle {cycle} failed\n{}\n{}",
|
||||
run.stdout,
|
||||
run.stderr
|
||||
);
|
||||
assert!(
|
||||
run.stdout.contains("1 event(s) kept local"),
|
||||
"cycle {cycle} should report it as kept, not skipped:\n{}",
|
||||
run.stdout
|
||||
);
|
||||
|
||||
let kept = fixture
|
||||
.server
|
||||
.stored("unified")
|
||||
.into_iter()
|
||||
.find(|item| item.contains("Haircut"))
|
||||
.expect("the event should still be in the aggregate");
|
||||
// Stripping it would let the default sink claim the event next cycle.
|
||||
assert!(kept.contains("@local"), "cycle {cycle}: {kept}");
|
||||
|
||||
assert!(
|
||||
!fixture
|
||||
.server
|
||||
.stored("work")
|
||||
.iter()
|
||||
.any(|item| item.contains("Haircut")),
|
||||
"cycle {cycle}: it reached the default sink anyway"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue