trails/apps/journal/app/lib/federation-delivery.server.test.ts
Ullrich Schäfer 20be961177 fix(journal): retract federated activities only on public→non-public
A received Delete is recorded by Mastodon as a permanent tombstone —
later Creates for the same URI are silently refused forever. The old
code sent Delete on every non-public save 'just in case' (the comment
even called over-sending harmless); on the 2026-06-07 soak this
tombstoned an unlisted activity's URI before its first real publish,
making its later flip to public invisible on the remote with no error
anywhere.

- visibilityTransitionAction(previous, next): Create on any transition
  to public (re-publish doubles as back-delivery; remotes dedupe by
  id), Delete only when leaving public, nothing for
  non-public→non-public.
- updateActivityVisibility reads the previous visibility and acts on
  the transition.
- design.md documents the tombstone permanence + the user-facing
  consequence (un-publish then re-publish won't resurrect the post on
  remotes that processed the retraction — same as Mastodon's own
  delete-and-redraft).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 08:52:23 +02:00

28 lines
1.3 KiB
TypeScript

import { describe, it, expect, vi } from "vitest";
// The db/boss imports in the module under test are irrelevant for the
// pure transition helper; stub them so importing the module is cheap.
vi.mock("./db.ts", () => ({ getDb: () => ({}) }));
vi.mock("./boss.server.ts", () => ({ enqueueOptional: vi.fn() }));
const { visibilityTransitionAction } = await import("./federation-delivery.server.ts");
describe("visibilityTransitionAction", () => {
it("publishes on any transition to public (including re-publish)", () => {
expect(visibilityTransitionAction("private", "public")).toBe("create");
expect(visibilityTransitionAction("unlisted", "public")).toBe("create");
expect(visibilityTransitionAction("public", "public")).toBe("create");
});
it("retracts only when leaving public", () => {
expect(visibilityTransitionAction("public", "private")).toBe("delete");
expect(visibilityTransitionAction("public", "unlisted")).toBe("delete");
});
it("stays silent for non-public to non-public — a Delete would tombstone the URI", () => {
expect(visibilityTransitionAction("private", "unlisted")).toBeNull();
expect(visibilityTransitionAction("unlisted", "private")).toBeNull();
expect(visibilityTransitionAction("private", "private")).toBeNull();
expect(visibilityTransitionAction("unlisted", "unlisted")).toBeNull();
});
});