A boost or a favourite given again is sent again

Every boost of a post was announce-{post}, and every favourite like-{post}:
after an Undo, giving it again made the same activity id, which the
delivery queue had already delivered and dropped (and a server that
remembers ids would ignore). Found by decePub's e2e actions against the
pasture, which unboost and boost again. Now each boost and favourite is an
activity of its own, as Mastodon's are, and an Undo names the one it ends;
a concurrent second boost is removed after saving. An edit's Update is
named to the millisecond, a quote approval's to the tick.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-05 02:48:35 +02:00
1 parent ce473f2741
commit b710493701
5 files changed
+64 -14

No files matched your search

+4 -3
View File
@@ -170,7 +170,7 @@ namespace PrivaPub.Tests.Http
var announce = items[0];
Assert.Equal("Announce", announce["type"]!.GetValue<string>());
Assert.Equal(reblog.ObjectURI, announce["id"]!.GetValue<string>());
Assert.Equal($"{persona.ActorUri()}/grunts/announce-{boosted.ID}", announce["id"]!.GetValue<string>());
Assert.Equal($"{persona.ActorUri()}/grunts/announce-{reblog.ID}", announce["id"]!.GetValue<string>());
Assert.Equal(boosted.ObjectURI, announce["object"]!.GetValue<string>());
Assert.Equal(persona.ActorUri(), announce["actor"]!.GetValue<string>());
@@ -436,7 +436,7 @@ namespace PrivaPub.Tests.Http
string Path(string id) => $"/peasants/{persona.UserName}/grunts/{id}";
var create = await _client.Fetch(Path($"create-{post.ID}"));
var announce = await _client.Fetch(Path($"announce-{original.ID}"));
var announce = await _client.Fetch(Path($"announce-{reblog.ID}"));
Assert.Equal(HttpStatusCode.OK, create.Status);
Assert.Equal(ActivityJson, create.MediaType);
@@ -451,7 +451,8 @@ namespace PrivaPub.Tests.Http
foreach (var unknown in new[] { $"create-{followersOnly.ID}", $"create-{Guid.NewGuid():N}"[..31], $"announce-{post.ID}", $"like-{post.ID}", "x" })
Assert.Equal(HttpStatusCode.NotFound, (await _client.Fetch(Path(unknown))).Status);
Assert.Equal(HttpStatusCode.NotFound, (await _client.Fetch($"/peasants/{friend.UserName}/grunts/announce-{original.ID}")).Status);
Assert.Equal(HttpStatusCode.NotFound, (await _client.Fetch(Path($"announce-{original.ID}"))).Status);
Assert.Equal(HttpStatusCode.NotFound, (await _client.Fetch($"/peasants/{friend.UserName}/grunts/announce-{reblog.ID}")).Status);
}
[Fact]
@@ -354,6 +354,37 @@ namespace PrivaPub.Tests.Http
Assert.True((await alice.Client.Post($"/api/v1/statuses/{quiet.Text("id")}/favourite")).Ok().Body.Flag("favourited"));
}
[Fact]
public async Task A_favourite_or_a_boost_given_again_after_its_undo_is_a_new_activity_that_is_delivered()
{
var since = DateTime.UtcNow.AddSeconds(-1);
var alice = await _host.Mastodon("alice");
var bob = new RemoteActor(_peer, "bob");
var remote = await _host.PublicPostFrom(bob, alice);
var path = $"/api/v1/statuses/{remote.ID}";
foreach (var (on, off) in new[] { ("favourite", "unfavourite"), ("reblog", "unreblog") })
{
(await alice.Client.Post($"{path}/{on}")).Ok();
(await alice.Client.Post($"{path}/{off}")).Ok();
(await alice.Client.Post($"{path}/{on}")).Ok();
}
var sent = await bob.Delivered(since);
foreach (var type in new[] { "Like", "Announce" })
{
var given = sent.Where(d => d.Type() == type).ToList();
Assert.Equal(2, given.Count);
Assert.NotEqual(given[0].Text("id"), given[1].Text("id"));
var undo = Assert.Single(sent, d => d.Type() == "Undo" && d["object"].Text("type") == type);
Assert.Equal(given[0].Text("id"), undo["object"].Text("id"));
}
var status = (await alice.Client.Get(path)).Ok().Body;
Assert.True(status.Flag("favourited"));
Assert.True(status.Flag("reblogged"));
Assert.Equal(1, status.Number("reblogs_count"));
}
[Fact]
public async Task Bookmarks_are_kept_listed_and_removed()
{