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:
randogoth 2026-09-10 14:30:25 +03:00
parent 7607078394
commit 47ad8b4c47
9 changed files with 298 additions and 26 deletions

View file

@ -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"
);
}
}