diff --git a/TODO.md b/TODO.md index 074f734..6b48c8c 100644 --- a/TODO.md +++ b/TODO.md @@ -52,9 +52,12 @@ the retrospective FEP-ae0c relay profiles. - Land and maintain `docs/FEP-AE0C-COMPATIBILITY.md` and the machine-readable fixture catalog before changing runtime behavior. -- Convert current Mastodon-style forwarding, LitePub reciprocal-follow and - Announce behavior, open publisher ingestion, NodeBB normalization, actor-path - compatibility, and authorized fetch into executable characterization tests. +- Convert current behavior into executable characterization tests. The first + fixture-driven API tranche covers traditional and LitePub relationship + handling, audience routing, exact-body forwarding, open publishers, NodeBB + normalization guards, unsupported activity handling, and actor-path + compatibility. Referenced-Announce and true two-relay reflection coverage + remain separate process-level integration work. - Resolve Public-in-`to` versus Public-only-in-`cc` through explicit configuration and real interoperability evidence rather than an implicit routing condition. diff --git a/api/fep_ae0c_characterization_test.go b/api/fep_ae0c_characterization_test.go new file mode 100644 index 0000000..fce335e --- /dev/null +++ b/api/fep_ae0c_characterization_test.go @@ -0,0 +1,585 @@ +// File: api/fep_ae0c_characterization_test.go + +package api + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "testing" + "time" + + "github.com/thystra/Activity-Relay/models" +) + +const fepAE0CPublic = "https://www.w3.org/ns/activitystreams#Public" + +type fepAE0CFixtureDocument struct { + Cases []fepAE0CFixture `json:"cases"` +} + +type fepAE0CFixture struct { + ID string `json:"id"` + Activity json.RawMessage `json:"activity"` + Actor json.RawMessage `json:"actor"` + HTTPRequest json.RawMessage `json:"http_request"` + Scenario json.RawMessage `json:"scenario"` +} + +func loadFEPAE0CFixture(t *testing.T, identifier string) fepAE0CFixture { + t.Helper() + + path := filepath.Join("..", "testdata", "fep-ae0c", "cases.json") + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read FEP-ae0c fixture catalog: %v", err) + } + + var document fepAE0CFixtureDocument + if err := json.Unmarshal(data, &document); err != nil { + t.Fatalf("decode FEP-ae0c fixture catalog: %v", err) + } + + for _, fixture := range document.Cases { + if fixture.ID == identifier { + return fixture + } + } + + t.Fatalf("FEP-ae0c fixture %q was not found", identifier) + return fepAE0CFixture{} +} + +func fixtureActivity(t *testing.T, identifier string) (*models.Activity, []byte) { + t.Helper() + + fixture := loadFEPAE0CFixture(t, identifier) + if len(fixture.Activity) == 0 { + t.Fatalf("fixture %q has no activity payload", identifier) + } + + var activity models.Activity + if err := json.Unmarshal(fixture.Activity, &activity); err != nil { + t.Fatalf("decode activity fixture %q: %v", identifier, err) + } + + return &activity, append([]byte(nil), fixture.Activity...) +} + +func fixtureActor(t *testing.T, identifier string) *models.Actor { + t.Helper() + + fixture := loadFEPAE0CFixture(t, identifier) + if len(fixture.Actor) == 0 { + t.Fatalf("fixture %q has no actor payload", identifier) + } + + var actor models.Actor + if err := json.Unmarshal(fixture.Actor, &actor); err != nil { + t.Fatalf("decode actor fixture %q: %v", identifier, err) + } + return &actor +} + +func actorForFixtureActivity(t *testing.T, activity *models.Activity, actorType string) *models.Actor { + t.Helper() + + actorURL, err := url.Parse(activity.Actor) + if err != nil || actorURL.Hostname() == "" { + t.Fatalf("fixture actor URL %q is invalid", activity.Actor) + } + + inbox := "https://" + actorURL.Host + "/inbox" + return &models.Actor{ + Context: []string{"https://www.w3.org/ns/activitystreams"}, + ID: activity.Actor, + Type: actorType, + PreferredUsername: "fixture", + Inbox: inbox, + Endpoints: &models.Endpoints{ + SharedInbox: inbox, + }, + } +} + +func postFEPAE0CFixture( + t *testing.T, + activity *models.Activity, + actor *models.Actor, + body []byte, +) *httptest.ResponseRecorder { + t.Helper() + + request := httptest.NewRequest( + http.MethodPost, + "/inbox", + bytes.NewReader(body), + ) + request.Header.Set("Content-Type", "application/activity+json") + recorder := httptest.NewRecorder() + + handleInbox( + recorder, + request, + mockActivityDecoderProvider(activity, actor), + ) + return recorder +} + +func resetFEPAE0CState(t *testing.T, domains ...string) { + t.Helper() + + removeRelayActivityTestKeys(t) + + ctx := context.Background() + keys := make([]string, 0, len(domains)*4) + for _, domain := range domains { + keys = append( + keys, + "relay:subscription:"+domain, + "relay:follower:"+domain, + "relay:publisher:"+domain, + "relay:pending:"+domain, + ) + } + if len(keys) > 0 { + if err := RelayState.RedisClient.Del(ctx, keys...).Err(); err != nil { + t.Fatalf("clear FEP-ae0c state: %v", err) + } + } + if err := RelayState.Load(); err != nil { + t.Fatalf("reload relay state: %v", err) + } +} + +func addFEPTraditionalReceiver(t *testing.T, domain string) { + t.Helper() + + RelayState.AddSubscriber(models.Subscriber{ + Domain: domain, + InboxURL: "https://" + domain + "/inbox", + ActorID: "https://" + domain + "/actor", + }) +} + +func waitForFEPStoredBody(t *testing.T, body []byte) { + t.Helper() + + ctx := context.Background() + deadline := time.Now().Add(3 * time.Second) + for time.Now().Before(deadline) { + keys, err := models.ScanKeys( + ctx, + RelayState.RedisClient, + "relay:activity:*", + ) + if err != nil { + t.Fatalf("scan stored relay activities: %v", err) + } + for _, key := range keys { + stored, err := RelayState.RedisClient.HGet( + ctx, + key, + "body", + ).Bytes() + if err == nil && bytes.Equal(stored, body) { + return + } + } + time.Sleep(25 * time.Millisecond) + } + t.Fatal("exact fixture body was not stored for relay fan-out") +} + +func requireNoFEPRelayActivity(t *testing.T) { + t.Helper() + + time.Sleep(250 * time.Millisecond) + keys, err := models.ScanKeys( + context.Background(), + RelayState.RedisClient, + "relay:activity:*", + ) + if err != nil { + t.Fatalf("scan stored relay activities: %v", err) + } + if len(keys) != 0 { + t.Fatalf("unexpected relay activity keys: %v", keys) + } +} + +func followerMutualStatus(domain string) (bool, bool) { + for _, follower := range RelayState.Snapshot().Followers { + if follower.Domain == domain { + return follower.MutuallyFollow, true + } + } + return false, false +} + +func TestFEPAE0CTraditionalFollowFixtures(t *testing.T) { + for _, identifier := range []string{ + "mastodon-follow-public", + "historical-state-property", + } { + t.Run(identifier, func(t *testing.T) { + const domain = "subscriber.example" + resetFEPAE0CState(t, domain) + t.Cleanup(func() { + resetFEPAE0CState(t, domain) + }) + + activity, body := fixtureActivity(t, identifier) + actor := actorForFixtureActivity(t, activity, "Application") + recorder := postFEPAE0CFixture(t, activity, actor, body) + + if recorder.Code != http.StatusAccepted { + t.Fatalf( + "status = %d; want %d", + recorder.Code, + http.StatusAccepted, + ) + } + if !RelayState.IsSubscriber(domain) { + t.Fatalf( + "traditional fixture %q did not create subscriber %s", + identifier, + domain, + ) + } + }) + } +} + +func TestFEPAE0CLitePubFollowAndAcceptFixtures(t *testing.T) { + const domain = "follower.example" + + t.Run("follow", func(t *testing.T) { + resetFEPAE0CState(t, domain) + t.Cleanup(func() { + resetFEPAE0CState(t, domain) + }) + + activity, body := fixtureActivity( + t, + "litepub-follow-relay-actor", + ) + activity.Object = RelayActor.ID + activity.To = []string{RelayActor.ID} + actor := actorForFixtureActivity(t, activity, "Application") + + recorder := postFEPAE0CFixture(t, activity, actor, body) + if recorder.Code != http.StatusAccepted { + t.Fatalf( + "status = %d; want %d", + recorder.Code, + http.StatusAccepted, + ) + } + if !RelayState.IsFollower(domain) { + t.Fatalf("LitePub follow did not create follower %s", domain) + } + mutual, found := followerMutualStatus(domain) + if !found { + t.Fatalf("follower %s is absent from snapshot", domain) + } + if mutual { + t.Fatalf("follower %s became mutual before Accept", domain) + } + }) + + t.Run("accept reciprocal follow", func(t *testing.T) { + resetFEPAE0CState(t, domain) + t.Cleanup(func() { + resetFEPAE0CState(t, domain) + }) + + activity, body := fixtureActivity( + t, + "litepub-accept-reciprocal-follow", + ) + actor := actorForFixtureActivity(t, activity, "Application") + RelayState.AddFollower(models.Follower{ + Domain: domain, + InboxURL: actor.Inbox, + ActivityID: "https://relay.example/activities/follow/1", + ActorID: actor.ID, + MutuallyFollow: false, + }) + + activity.To = []string{RelayActor.ID} + inner, ok := activity.Object.(map[string]interface{}) + if !ok { + t.Fatal("Accept fixture object is not an embedded activity") + } + inner["actor"] = RelayActor.ID + inner["object"] = actor.ID + + recorder := postFEPAE0CFixture(t, activity, actor, body) + if recorder.Code != http.StatusAccepted { + t.Fatalf( + "status = %d; want %d", + recorder.Code, + http.StatusAccepted, + ) + } + mutual, found := followerMutualStatus(domain) + if !found { + t.Fatalf("follower %s is absent from snapshot", domain) + } + if !mutual { + t.Fatalf("follower %s was not finalized as mutual", domain) + } + }) +} + +func TestFEPAE0CTraditionalPublicAudienceFixtures(t *testing.T) { + for _, identifier := range []string{ + "traditional-create-public-to", + "traditional-create-public-cc", + } { + t.Run(identifier, func(t *testing.T) { + const ( + sourceDomain = "publisher.example" + receiverDomain = "receiver.example" + ) + resetFEPAE0CState( + t, + sourceDomain, + receiverDomain, + ) + t.Cleanup(func() { + resetFEPAE0CState( + t, + sourceDomain, + receiverDomain, + ) + }) + addFEPTraditionalReceiver(t, receiverDomain) + + activity, body := fixtureActivity(t, identifier) + actor := actorForFixtureActivity(t, activity, "Person") + recorder := postFEPAE0CFixture(t, activity, actor, body) + + if recorder.Code != http.StatusAccepted { + t.Fatalf( + "status = %d; want %d", + recorder.Code, + http.StatusAccepted, + ) + } + waitForFEPStoredBody(t, body) + if !RelayState.IsPublisher(sourceDomain) { + t.Fatalf("source %s was not recorded as publisher", sourceDomain) + } + }) + } +} + +func TestFEPAE0CPreservesOriginalBodyWithLinkedDataProof(t *testing.T) { + const ( + sourceDomain = "publisher.example" + receiverDomain = "receiver.example" + ) + resetFEPAE0CState(t, sourceDomain, receiverDomain) + t.Cleanup(func() { + resetFEPAE0CState(t, sourceDomain, receiverDomain) + }) + addFEPTraditionalReceiver(t, receiverDomain) + + activity, body := fixtureActivity( + t, + "traditional-forward-preserves-ld-proof", + ) + if !bytes.Contains(body, []byte(`"RsaSignature2017"`)) { + t.Fatal("LD-proof fixture does not contain RsaSignature2017") + } + actor := actorForFixtureActivity(t, activity, "Person") + recorder := postFEPAE0CFixture(t, activity, actor, body) + + if recorder.Code != http.StatusAccepted { + t.Fatalf( + "status = %d; want %d", + recorder.Code, + http.StatusAccepted, + ) + } + waitForFEPStoredBody(t, body) +} + +func TestFEPAE0CPublicAnnounceNonFanoutFixtures(t *testing.T) { + for _, identifier := range []string{ + "public-announce-url-only", + "public-announce-cross-origin-embedded", + } { + t.Run(identifier, func(t *testing.T) { + const ( + sourceDomain = "publisher.example" + receiverDomain = "receiver.example" + ) + resetFEPAE0CState( + t, + sourceDomain, + receiverDomain, + ) + t.Cleanup(func() { + resetFEPAE0CState( + t, + sourceDomain, + receiverDomain, + ) + }) + addFEPTraditionalReceiver(t, receiverDomain) + + activity, body := fixtureActivity(t, identifier) + actor := actorForFixtureActivity(t, activity, "Person") + recorder := postFEPAE0CFixture(t, activity, actor, body) + + if recorder.Code != http.StatusAccepted { + t.Fatalf( + "status = %d; want %d", + recorder.Code, + http.StatusAccepted, + ) + } + requireNoFEPRelayActivity(t) + publisher := RelayState.SelectPublisher(sourceDomain) + if publisher == nil { + t.Fatalf("source %s was not recorded as publisher", sourceDomain) + } + if publisher.LastActivityType != "Announce" { + t.Fatalf( + "last publisher activity = %q; want Announce", + publisher.LastActivityType, + ) + } + }) + } +} + +func TestFEPAE0CUnsupportedPublicLikeIsAcknowledgedWithoutFanout(t *testing.T) { + const ( + sourceDomain = "publisher.example" + receiverDomain = "receiver.example" + ) + resetFEPAE0CState(t, sourceDomain, receiverDomain) + t.Cleanup(func() { + resetFEPAE0CState(t, sourceDomain, receiverDomain) + }) + addFEPTraditionalReceiver(t, receiverDomain) + + activity, body := fixtureActivity(t, "unsupported-public-like") + actor := actorForFixtureActivity(t, activity, "Person") + recorder := postFEPAE0CFixture(t, activity, actor, body) + + if recorder.Code != http.StatusAccepted { + t.Fatalf( + "status = %d; want %d", + recorder.Code, + http.StatusAccepted, + ) + } + requireNoFEPRelayActivity(t) + if RelayState.IsPublisher(sourceDomain) { + t.Fatalf( + "unsupported public Like unexpectedly recorded publisher %s", + sourceDomain, + ) + } +} + +func TestFEPAE0COpenPublisherCreate(t *testing.T) { + const ( + sourceDomain = "blog.example" + receiverDomain = "receiver.example" + ) + resetFEPAE0CState(t, sourceDomain, receiverDomain) + t.Cleanup(func() { + resetFEPAE0CState(t, sourceDomain, receiverDomain) + }) + addFEPTraditionalReceiver(t, receiverDomain) + + activity, body := fixtureActivity(t, "open-publisher-public-create") + actor := actorForFixtureActivity(t, activity, "Person") + recorder := postFEPAE0CFixture(t, activity, actor, body) + + if recorder.Code != http.StatusAccepted { + t.Fatalf( + "status = %d; want %d", + recorder.Code, + http.StatusAccepted, + ) + } + if RelayState.IsSubscriber(sourceDomain) || + RelayState.IsFollower(sourceDomain) { + t.Fatalf( + "open publisher %s unexpectedly became a receiver", + sourceDomain, + ) + } + waitForFEPStoredBody(t, body) + if !RelayState.IsPublisher(sourceDomain) { + t.Fatalf("open publisher %s was not recorded", sourceDomain) + } +} + +func TestFEPAE0CServerActorArbitraryPath(t *testing.T) { + actor := fixtureActor(t, "server-actor-arbitrary-path") + if !isActorAbleToBeFollower(actor) { + t.Fatalf( + "Application actor at implementation-defined path %q was rejected", + actor.ID, + ) + } +} + +func TestFEPAE0CNodeBBNormalizationGuardFixtures(t *testing.T) { + tests := []struct { + identifier string + want bool + }{ + { + identifier: "nodebb-embedded-announce-same-origin", + want: true, + }, + { + identifier: "public-announce-url-only", + want: false, + }, + { + identifier: "public-announce-cross-origin-embedded", + want: false, + }, + } + + for _, test := range tests { + t.Run(test.identifier, func(t *testing.T) { + activity, _ := fixtureActivity(t, test.identifier) + if got := shouldFanOutPublicAnnounce(activity); got != test.want { + t.Fatalf( + "shouldFanOutPublicAnnounce(%s) = %v; want %v", + test.identifier, + got, + test.want, + ) + } + }) + } +} + +func TestFEPAE0CFixturePublicURIConstant(t *testing.T) { + activity, _ := fixtureActivity(t, "traditional-create-public-to") + if !contains(activity.To, fepAE0CPublic) { + t.Fatalf( + "fixture does not address expected Public URI %q", + fepAE0CPublic, + ) + } +} + +// EOF: api/fep_ae0c_characterization_test.go diff --git a/contrib/ops/test_fep_ae0c_cases.py b/contrib/ops/test_fep_ae0c_cases.py index 8b68f2e..461e6d7 100644 --- a/contrib/ops/test_fep_ae0c_cases.py +++ b/contrib/ops/test_fep_ae0c_cases.py @@ -11,6 +11,7 @@ REPOSITORY_ROOT = Path(__file__).resolve().parents[2] CASES_PATH = REPOSITORY_ROOT / "testdata" / "fep-ae0c" / "cases.json" +COVERAGE_PATH = REPOSITORY_ROOT / "testdata" / "fep-ae0c" / "coverage.json" REQUIRED_CASE_FIELDS = { "id", @@ -24,6 +25,7 @@ } PAYLOAD_FIELDS = {"activity", "actor", "http_request", "scenario"} +EXECUTABLE_COVERAGE = {"executable-new", "existing-executable"} class FEPAE0CCasesTest(unittest.TestCase): @@ -32,6 +34,10 @@ def setUpClass(cls) -> None: cls.document = json.loads(CASES_PATH.read_text(encoding="utf-8")) cls.allowed = cls.document["allowed_values"] cls.cases = cls.document["cases"] + cls.coverage_document = json.loads( + COVERAGE_PATH.read_text(encoding="utf-8") + ) + cls.coverage = cls.coverage_document["coverage"] def test_document_metadata(self) -> None: self.assertEqual(self.document["schema_version"], 1) @@ -94,6 +100,48 @@ def test_audit_required_cases_are_not_claimed_as_covered(self) -> None: with self.subTest(case=case["id"]): self.assertEqual(case["decision"], "investigate") + def test_coverage_metadata(self) -> None: + self.assertEqual(self.coverage_document["schema_version"], 1) + self.assertEqual( + self.coverage_document["activity_relay_baseline"], + "v2.5.0", + ) + self.assertEqual( + self.coverage_document["fixture_catalog"], + "testdata/fep-ae0c/cases.json", + ) + + def test_every_fixture_has_exactly_one_coverage_record(self) -> None: + case_ids = {case["id"] for case in self.cases} + coverage_ids = [entry["case_id"] for entry in self.coverage] + + self.assertEqual(len(coverage_ids), len(set(coverage_ids))) + self.assertEqual(case_ids, set(coverage_ids)) + + def test_coverage_statuses_and_test_names(self) -> None: + allowed_statuses = set( + self.coverage_document["allowed_statuses"] + ) + + for entry in self.coverage: + with self.subTest(case=entry["case_id"]): + self.assertIn(entry["status"], allowed_statuses) + self.assertIsInstance(entry.get("notes"), str) + self.assertTrue(entry["notes"].strip()) + + tests = entry.get("tests") + self.assertIsInstance(tests, list) + + if entry["status"] in EXECUTABLE_COVERAGE: + self.assertGreater(len(tests), 0) + for test_name in tests: + self.assertRegex( + test_name, + r"^Test[A-Za-z0-9_]+$", + ) + else: + self.assertEqual(tests, []) + if __name__ == "__main__": unittest.main() diff --git a/docs/FEP-AE0C-COMPATIBILITY.md b/docs/FEP-AE0C-COMPATIBILITY.md index d73b861..8a5766f 100644 --- a/docs/FEP-AE0C-COMPATIBILITY.md +++ b/docs/FEP-AE0C-COMPATIBILITY.md @@ -230,6 +230,41 @@ executable unit and multi-relay integration coverage. receiver-presentation integration tests. 8. Select 3.0 defaults only after the compatibility evidence is recorded. +## Executable characterization coverage + +The first executable characterization tranche is mapped in +[`testdata/fep-ae0c/coverage.json`](../testdata/fep-ae0c/coverage.json). + +Fixture-driven API tests now freeze: + +- traditional Public-collection subscription; +- LitePub-style follow and reciprocal `Accept`; +- v2.5 forwarding for Public in `to` and Public only in `cc`; +- byte-for-byte forwarding of a body containing an existing + `RsaSignature2017` proof, without claiming proof verification; +- open publisher fan-out without receiver membership; +- HTTP 202 without fan-out for unsupported public `Like`; +- publisher-only handling for URL-only and foreign-origin public + `Announce`; +- the same-origin NodeBB normalization guard; and +- `Application` actors at implementation-defined paths. + +Existing tests provide the signed authorized-fetch coverage referenced by the +fixture catalog. + +Two cases remain intentionally outside this tranche: + +1. `litepub-announce-reference` needs a signed remote object and actor server, + plus assertions at real receiver boundaries. +2. `repeat-id-two-relay-loop` needs two independently configured relay + processes and independent Redis databases. An in-memory graph simulation + would not prove the actual storage, queue, signature, retry, or reflection + behavior and is therefore not accepted as completion evidence. + +The RFC 9421 dual-profile fixture remains future protocol coverage. External +review feedback may add or refine fixtures without changing these frozen v2.5 +characterization assertions. + ## Fixture status The initial fixtures are specifications. They intentionally include both covered diff --git a/docs/INTEGRATION-TESTING.md b/docs/INTEGRATION-TESTING.md index 672dab1..5d64a3b 100644 --- a/docs/INTEGRATION-TESTING.md +++ b/docs/INTEGRATION-TESTING.md @@ -138,6 +138,17 @@ characterization tests. For changes involving direct forwarding, reciprocal follows, Announce wrapping, audience handling, or loop prevention, exercise both relay families and a mixed two-relay topology. +### FEP-ae0c characterization coverage + +`testdata/fep-ae0c/coverage.json` must contain exactly one coverage record for +every fixture. Unit-characterized cases must name executable Go tests. +Network-dependent and future-protocol cases must remain explicitly classified +rather than being represented by mocks that bypass the behavior under review. + +The referenced-Announce and two-relay reflection cases require real process +boundaries, separate Redis state, signed HTTP requests, and bounded delivery +assertions before their coverage status may change to executable. + ## Promotion gate Do not tag a release candidate until both deployment forms pass the applicable diff --git a/testdata/fep-ae0c/coverage.json b/testdata/fep-ae0c/coverage.json new file mode 100644 index 0000000..29fdd53 --- /dev/null +++ b/testdata/fep-ae0c/coverage.json @@ -0,0 +1,148 @@ +{ + "schema_version": 1, + "activity_relay_baseline": "v2.5.0", + "fixture_catalog": "testdata/fep-ae0c/cases.json", + "allowed_statuses": [ + "executable-new", + "existing-executable", + "planned-integration", + "future-protocol" + ], + "coverage": [ + { + "case_id": "mastodon-follow-public", + "status": "executable-new", + "tests": [ + "TestFEPAE0CTraditionalFollowFixtures" + ], + "notes": "Fixture-driven handler test creates a traditional subscriber." + }, + { + "case_id": "litepub-follow-relay-actor", + "status": "executable-new", + "tests": [ + "TestFEPAE0CLitePubFollowAndAcceptFixtures" + ], + "notes": "Fixture-driven handler test creates a non-mutual follower." + }, + { + "case_id": "litepub-accept-reciprocal-follow", + "status": "executable-new", + "tests": [ + "TestFEPAE0CLitePubFollowAndAcceptFixtures" + ], + "notes": "Fixture-driven handler test finalizes mutual-follow state." + }, + { + "case_id": "traditional-create-public-to", + "status": "executable-new", + "tests": [ + "TestFEPAE0CTraditionalPublicAudienceFixtures" + ], + "notes": "Exact original fixture bytes are stored for traditional fan-out." + }, + { + "case_id": "traditional-create-public-cc", + "status": "executable-new", + "tests": [ + "TestFEPAE0CTraditionalPublicAudienceFixtures" + ], + "notes": "Freezes v2.5 behavior that Public only in cc also enters fan-out." + }, + { + "case_id": "traditional-forward-preserves-ld-proof", + "status": "executable-new", + "tests": [ + "TestFEPAE0CPreservesOriginalBodyWithLinkedDataProof" + ], + "notes": "Proves byte preservation without claiming proof verification." + }, + { + "case_id": "litepub-announce-reference", + "status": "planned-integration", + "tests": [], + "notes": "Requires a signed remote object/actor server and receiver assertions." + }, + { + "case_id": "nodebb-embedded-announce-same-origin", + "status": "executable-new", + "tests": [ + "TestFEPAE0CNodeBBNormalizationGuardFixtures", + "TestExecutePublicAnnounceFansOutRelaySignedWrapperToAllReceivers" + ], + "notes": "Fixture guard is bound to the existing full wrapper fan-out test." + }, + { + "case_id": "public-announce-url-only", + "status": "executable-new", + "tests": [ + "TestFEPAE0CPublicAnnounceNonFanoutFixtures", + "TestFEPAE0CNodeBBNormalizationGuardFixtures" + ], + "notes": "Acknowledged and recorded as publisher activity without fan-out." + }, + { + "case_id": "public-announce-cross-origin-embedded", + "status": "executable-new", + "tests": [ + "TestFEPAE0CPublicAnnounceNonFanoutFixtures", + "TestFEPAE0CNodeBBNormalizationGuardFixtures" + ], + "notes": "Freezes the same-origin anti-amplification restriction." + }, + { + "case_id": "unsupported-public-like", + "status": "executable-new", + "tests": [ + "TestFEPAE0CUnsupportedPublicLikeIsAcknowledgedWithoutFanout" + ], + "notes": "Freezes HTTP 202 without fan-out or publisher accounting." + }, + { + "case_id": "open-publisher-public-create", + "status": "executable-new", + "tests": [ + "TestFEPAE0COpenPublisherCreate" + ], + "notes": "Publisher is recorded and fanned out without becoming a receiver." + }, + { + "case_id": "server-actor-arbitrary-path", + "status": "executable-new", + "tests": [ + "TestFEPAE0CServerActorArbitraryPath", + "TestIsActorAbleToBeFollower" + ], + "notes": "Binds the fixture to existing actor-path compatibility behavior." + }, + { + "case_id": "authorized-fetch-canonical-object", + "status": "existing-executable", + "tests": [ + "TestNewActivityPubActivityFromRemoteActivitySignsGET", + "TestSignGETUsesExactWireAuthority" + ], + "notes": "Existing tests verify signed GET and exact wire authority." + }, + { + "case_id": "historical-state-property", + "status": "executable-new", + "tests": [ + "TestFEPAE0CTraditionalFollowFixtures" + ], + "notes": "Unknown state property does not prevent traditional subscription." + }, + { + "case_id": "repeat-id-two-relay-loop", + "status": "planned-integration", + "tests": [], + "notes": "Must use two real relay processes and independent Redis databases." + }, + { + "case_id": "rfc9421-dual-profile-post", + "status": "future-protocol", + "tests": [], + "notes": "Becomes executable only when additive RFC 9421/RFC 9530 code exists." + } + ] +}