diff --git a/CLAUDE.md b/CLAUDE.md
index 0391d65..9ed6360 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -132,6 +132,30 @@ Critical path for MVP: Stages 1–5, 7–8, 10–11
xunit.v3 is built against Microsoft.Testing.Platform 1.x; anything 2.x makes every test run die at
startup with a `TypeLoadException`. See the comment in `Directory.Packages.props`.
+### Cross-store compliance suites
+
+Event sourcing behavior that Polecat and Marten both have belongs in
+`JasperFx.Events.ComplianceTests` (source-only package; the suites compile into `Polecat.Tests` so
+JasperFx's aggregate source generator binds Polecat's session types). Enroll a suite with an empty
+subclass in `Compliance/polecat_event_store_compliance.cs`, implement whatever seam it needs on
+`PolecatComplianceFixture`, and delete the Polecat-local copy. Ten suites are enrolled across three
+waves (#393, #400, #399).
+
+To iterate on a suite before the JasperFx release, build against a working copy instead of the
+package:
+
+```
+dotnet build src/Polecat.Tests/Polecat.Tests.csproj \
+ -p:ComplianceSourceDir=$HOME/code/jasperfx/src/JasperFx.Events.ComplianceTests
+```
+
+**Two markers, opposite meanings.** `ported:` on a test file means it *duplicates* a test that also
+exists in another store and should eventually move into a shared suite — one file carries it today,
+`Linq/additional_linq_operator_tests.cs`, blocked because there is no document-db compliance library.
+A bare `marten#NNNN` citation means the opposite: Polecat implemented the same feature and the issue
+number is provenance. Do not treat a `marten#NNNN` count as a porting backlog; it grows with every
+parity feature and never converges.
+
### Writing tests that survive being run in parallel processes
The suite is a candidate for being run across several worker processes at once (Bobcat's supervisor
diff --git a/Directory.Packages.props b/Directory.Packages.props
index d4a4fe7..6a417de 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -61,15 +61,27 @@
Also carries the jasperfx#594 patch half (a timed-out blue/green side-effect gate
re-reads progression and succeeds when the replay had already reached the mark, plus a
configurable DaemonSettings.SideEffectGateTimeout), jasperfx#595 (BatchingChannel could
- deliver its trailing batch twice on shutdown) and #597. Same lockstep rule as above. -->
-
-
+ deliver its trailing batch twice on shutdown) and #597. Same lockstep rule as above.
+ JasperFx 2.38.0: (a) jasperfx#613 — compliance wave 3 (polecat#399):
+ JasperFx.Events.ComplianceTests gains ActivityCorrelationCompliance and
+ StringIdentitySingleStreamCompliance, plus the seam those needed
+ (ComplianceStoreConfig.StreamIdentity/.EnableCorrelationTracking and the
+ CorrelationIdFor/CausationIdFor/SetCorrelationId fixture members). That is the half this
+ bump consumes. (b) jasperfx#616 — the ProjectionScenario test harness lifted out of
+ Marten into a new JasperFx.Events.TestSupport namespace, which is what polecat#404 will
+ consume to delete Polecat's seven-file twin; nothing in this bump depends on it, and the
+ namespaces do not collide. (c) jasperfx#611 (EventProjection published types discovered
+ semantically rather than syntactically), jasperfx#615 (the blue/green side-effect gate
+ warm-up moved off the agent start path, #598/#610) and jasperfx#617 (container-scoped
+ projections usable by live aggregation, marten#5095). Same lockstep rule as above. -->
+
+
-
+
-
+
@@ -160,16 +172,20 @@
Weasel 9.23.1 (weasel#420): the weasel#416 identifier validation finally reaches the SQL
Server migrator, which validated NOTHING before this — the directly relevant half for
Polecat. Companion providers (Oracle, MySQL, Sqlite) fixed in the same pass, and
- PostgreSQL's copy folded onto the shared Weasel.Core helper (weasel#422). -->
-
-
-
+ PostgreSQL's copy folded onto the shared Weasel.Core helper (weasel#422).
+ Weasel 9.23.2: weasel#425 — carries the JasperFx 2.38.0 floor, which is the reason to
+ take it; Polecat pins the family in lockstep rather than letting it resolve
+ transitively. Also weasel#423, where Sqlite and MySql finally implement the non-generic
+ Weasel.Core.ICommandBuilder — no SQL Server surface touched. -->
+
+
+
-
+
diff --git a/docs/projection-sg-audit-108.md b/docs/projection-sg-audit-108.md
index 4481e98..3080417 100644
--- a/docs/projection-sg-audit-108.md
+++ b/docs/projection-sg-audit-108.md
@@ -17,6 +17,12 @@
## How to read this report
+- Types marked *(shared: SomethingCompliance)* are declared in
+ `JasperFx.Events.ComplianceTests` rather than in a Polecat test file. That
+ package is source-only, so they compile inside `Polecat.Tests` and the source
+ generator treats them exactly like local types. They moved there in compliance
+ waves 1–3 (#393, #400, #399); everything else cites the file it lives in.
+
- **✅ Generated**: SG emits a `[GeneratedEvolver]` partial / standalone
evolver / `EventProjection` partial override; runtime registration finds
the dispatcher; the post-#276 fail-fast does not fire.
@@ -59,16 +65,16 @@ methods and emits a standalone `TEvolver` class.
| 9 | `Report` *(Events/project_latest_tests.cs)* | `Guid` | Apply + Create | ✓ | `..._Report_System_GuidEvolver.g.cs` | ✅ |
| 10 | `StringReport` *(Events/project_latest_tests.cs)* | `string` | Apply + Create | ✓ | `..._StringReport_stringEvolver.g.cs` | ✅ |
| 11 | `ScenarioQuestParty` *(Events/projection_scenario_tests.cs)* | `Guid` | Apply + Create | ✓ | `..._ScenarioQuestParty_System_GuidEvolver.g.cs` | ✅ |
-| 12 | `MutableIEventEvolveAggregate` *(Events/self_aggregating_evolve_method.cs)* | `Guid` | `Evolve(snapshot, IEvent)` | ✗ | `..._MutableIEventEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ |
-| 13 | `MutableObjectEvolveAggregate` *(Events/self_aggregating_evolve_method.cs)* | `Guid` | `Evolve(snapshot, TEvent)` | ✗ | `..._MutableObjectEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ |
-| 14 | `ImmutableIEventEvolveAggregate` *(record; Events/self_aggregating_evolve_method.cs)* | `Guid` | `Evolve(snapshot, IEvent)` returning new record | ✗ | `..._ImmutableIEventEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ |
+| 12 | `MutableIEventEvolveAggregate` *(shared: SelfAggregatingEvolveCompliance)* | `Guid` | `Evolve(snapshot, IEvent)` | ✗ | `..._MutableIEventEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ |
+| 13 | `MutableObjectEvolveAggregate` *(shared: SelfAggregatingEvolveCompliance)* | `Guid` | `Evolve(snapshot, TEvent)` | ✗ | `..._MutableObjectEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ |
+| 14 | `ImmutableIEventEvolveAggregate` *(record; shared: SelfAggregatingEvolveCompliance)* | `Guid` | `Evolve(snapshot, IEvent)` returning new record | ✗ | `..._ImmutableIEventEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ |
| 15 | `ImmutableObjectEvolveAggregate` *(record; same file)* | `Guid` | `Evolve(snapshot, TEvent)` returning new record | ✗ | `..._ImmutableObjectEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ |
-| 16 | `AsyncEvolveAggregate` *(Events/self_aggregating_evolve_method.cs)* | `Guid` | `EvolveAsync` | ✗ | `..._AsyncEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ |
+| 16 | `AsyncEvolveAggregate` *(shared: SelfAggregatingEvolveCompliance)* | `Guid` | `EvolveAsync` | ✗ | `..._AsyncEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ |
| 17 | `ImmutableAsyncEvolveAggregate` *(record; same file)* | `Guid` | `EvolveAsync` returning new record | ✗ | `..._ImmutableAsyncEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ |
| 18 | `CompositeQuestParty` *(Projections/composite_projection_tests.cs)* | `Guid` | Apply + Create | ✓ | `..._CompositeQuestParty_System_GuidEvolver.g.cs` | ✅ |
| 19 | `QuestStats` *(Projections/composite_projection_tests.cs)* | `Guid` | Apply + Create | ✓ | `..._QuestStats_System_GuidEvolver.g.cs` | ✅ |
| 20 | `QuestParty` *(Projections/inline_projection_tests.cs)* | `Guid` | Apply + Create + ShouldDelete | ✓ | `..._QuestParty_System_GuidEvolver.g.cs` | ✅ |
-| 21 | `SelfAggregatingStringQuest` *(Projections/single_stream_projection_with_string_identity_tests.cs)* | `string` | Apply + Create | ✓ | `..._SelfAggregatingStringQuest_stringEvolver.g.cs` | ✅ |
+| 21 | `SelfAggregatingStringQuest` *(shared: StringIdentitySingleStreamCompliance)* | `string` | Apply + Create | ✓ | `..._SelfAggregatingStringQuest_stringEvolver.g.cs` | ✅ |
| 22 | `SnapshotParty` *(Projections/snapshot_registration_tests.cs)* | `Guid` | Apply + Create | ✓ | `..._SnapshotParty_System_GuidEvolver.g.cs` | ✅ |
| 23 | `SnapshotPartyByString` *(Projections/snapshot_registration_tests.cs)* | `string` | Apply + Create | ✓ | `..._SnapshotPartyByString_stringEvolver.g.cs` | ✅ |
| 24 | `Payment` *(Projections/using_guid_based_strong_typed_id_for_aggregate_identity.cs)* | `PaymentId` *(wraps Guid)* | Apply + Create | ✓ | `..._Payment_Polecat_Tests_Projections_PaymentIdEvolver.g.cs` | ✅ |
@@ -90,13 +96,13 @@ override) — **`partial` required on the class** for these shapes.
|---|---|---|---|---|---|---|
| 1 | `QuestLogProjection` *(Projections/event_projection_tests.cs)* | `EventProjection` | conventional `Project(TEvent, IDocumentSession)` | ✓ | `..._QuestLogProjection.EventProjection.g.cs` | ✅ |
| 2 | `MultiEventQuestLogProjection` *(Projections/event_projection_tests.cs)* | `EventProjection` | conventional `Project(TEvent, IDocumentSession)` × 2 | ✓ | `..._MultiEventQuestLogProjection.EventProjection.g.cs` | ✅ |
-| 3 | `SimpleEnrichmentProjection` *(Projections/event_projection_enrichment_tests.cs)* | `EventProjection` | `Project` + `EnrichEventsAsync` override | ✓ | `..._SimpleEnrichmentProjection.EventProjection.g.cs` | ✅ (EnrichEventsAsync is a side-channel; dispatch still through SG-emitted `Project`) |
-| 4 | `EnrichmentCallOrderProjection` *(Projections/event_projection_enrichment_tests.cs)* | `EventProjection` | `Project` + `EnrichEventsAsync` override | ✓ | `..._EnrichmentCallOrderProjection.EventProjection.g.cs` | ✅ |
-| 5 | `DbLookupEnrichmentProjection` *(Projections/event_projection_enrichment_tests.cs)* | `EventProjection` | `Project` + `EnrichEventsAsync` override | ✓ | `..._DbLookupEnrichmentProjection.EventProjection.g.cs` | ✅ |
-| 6 | `AuditRecordProjection` *(Projections/event_projection_should_register_document_types.cs)* | `EventProjection` | overrides `ApplyAsync` directly | ✓ | `..._AuditRecordProjection.TypeRegistration.g.cs` *(TypeRegistration-only)* | ✅ — dispatch via `ApplyAsync` override; SG emits only the `RegisterPublishedType` constructor so `Schema.For` is discoverable (per [marten#4166](https://github.com/JasperFx/marten/issues/4166)) |
-| 7 | `AuditRecordCreatorProjection` *(Projections/event_projection_should_register_document_types.cs)* | `EventProjection` | conventional `Create(AuditableEvent)` returning the doc type | ✓ | `..._AuditRecordCreatorProjection.EventProjection.g.cs` | ✅ |
+| 3 | `SimpleEnrichmentProjection` *(shared: EventProjectionEnrichmentCompliance)* | `EventProjection` | `Project` + `EnrichEventsAsync` override | ✓ | `..._SimpleEnrichmentProjection.EventProjection.g.cs` | ✅ (EnrichEventsAsync is a side-channel; dispatch still through SG-emitted `Project`) |
+| 4 | `EnrichmentCallOrderProjection` *(shared: EventProjectionEnrichmentCompliance)* | `EventProjection` | `Project` + `EnrichEventsAsync` override | ✓ | `..._EnrichmentCallOrderProjection.EventProjection.g.cs` | ✅ |
+| 5 | `DbLookupEnrichmentProjection` *(shared: EventProjectionEnrichmentCompliance)* | `EventProjection` | `Project` + `EnrichEventsAsync` override | ✓ | `..._DbLookupEnrichmentProjection.EventProjection.g.cs` | ✅ |
+| 6 | `AuditRecordProjection` *(shared: EventProjectionRegistrationCompliance)* | `EventProjection` | overrides `ApplyAsync` directly | ✓ | `..._AuditRecordProjection.TypeRegistration.g.cs` *(TypeRegistration-only)* | ✅ — dispatch via `ApplyAsync` override; SG emits only the `RegisterPublishedType` constructor so `Schema.For` is discoverable (per [marten#4166](https://github.com/JasperFx/marten/issues/4166)) |
+| 7 | `AuditRecordCreatorProjection` *(shared: EventProjectionRegistrationCompliance)* | `EventProjection` | conventional `Create(AuditableEvent)` returning the doc type | ✓ | `..._AuditRecordCreatorProjection.EventProjection.g.cs` | ✅ |
| 8 | `ImportSqlProjection` *(Projections/using_event_projection_for_flat_tables.cs)* | `EventProjection` | conventional `Project(TEvent, IDocumentSession)` × 2 | ✓ | `..._ImportSqlProjection.EventProjection.g.cs` | ✅ |
-| 9 | `StringQuestPartyProjection` *(Projections/single_stream_projection_with_string_identity_tests.cs)* | `SingleStreamProjection` | conventional Apply/Create + ShouldDelete on the **projection** (delegating to doc would be the alternative) | ✓ | `..._StringQuestPartyProjection.Evolver.g.cs` *(PartialProjection)* | ✅ |
+| 9 | `StringQuestPartyProjection` *(shared: StringIdentitySingleStreamCompliance)* | `SingleStreamProjection` | conventional Apply/Create + ShouldDelete on the **projection** (delegating to doc would be the alternative) | ✓ | `..._StringQuestPartyProjection.Evolver.g.cs` *(PartialProjection)* | ✅ |
| 10 | `CustomerSummaryProjection` *(Projections/multi_stream_projection_tests.cs)* | `MultiStreamProjection` | conventional Apply on the projection | ✓ | `..._CustomerSummaryProjection.Evolver.g.cs` | ✅ |
| 11 | `MonthlyAccountActivityProjection` *(Projections/time_based_multi_stream_projection_tests.cs)* | `MultiStreamProjection` | conventional Apply on the projection | ✓ | `..._MonthlyAccountActivityProjection.Evolver.g.cs` | ✅ |
| 12 | `CompositeOrderProjection` *(Projections/composite_try_find_upstream_cache_tests.cs)* | `SingleStreamProjection` | **overrides `Evolve` directly** | ✓ | — | ⚠ Deliberate bypass — override wins per #276 doctrine. Composite-projection test exercises upstream-cache lookup, not dispatch correctness. |
@@ -151,15 +157,15 @@ through the projection and asserts state evolves correctly:
|---|---|
| Self-aggregating sync Apply + Create | `fetch_for_writing_tests`, `Bug_4197_fetch_for_writing_natural_key` |
| Self-aggregating Apply + Create + **ShouldDelete** | `aggregate_stream_to_last_known_tests` (DeletableAggregate), `inline_projection_tests` (QuestParty) |
-| Self-aggregating sync `Evolve(snapshot, IEvent)` (mutable + immutable record) | `self_aggregating_evolve_method` (×4 variants) |
-| Self-aggregating `EvolveAsync` | `self_aggregating_evolve_method` (×2 variants) |
-| String identity | `single_stream_projection_with_string_identity_tests`, `project_latest_tests` (StringReport), `always_enforce_consistency_with_string_stream_id` |
+| Self-aggregating sync `Evolve(snapshot, IEvent)` (mutable + immutable record) | `SelfAggregatingEvolveCompliance` (×4 variants) |
+| Self-aggregating `EvolveAsync` | `SelfAggregatingEvolveCompliance` (×2 variants) |
+| String identity | `StringIdentitySingleStreamCompliance`, `project_latest_tests` (StringReport), `always_enforce_consistency_with_string_stream_id` |
| Strong-typed-id identity (wrapper struct) | `using_guid_based_strong_typed_id_for_aggregate_identity`, `using_string_based_strong_typed_id_for_aggregate_identity` |
| `EventProjection` with conventional `Project(TEvent, IDocumentSession)` | `event_projection_tests` |
-| `EventProjection` with `Create(TEvent)` returning a doc | `event_projection_should_register_document_types` (AuditRecordCreatorProjection) |
-| `EventProjection` with `ApplyAsync` override | `event_projection_should_register_document_types` (AuditRecordProjection) |
-| `EventProjection` with `EnrichEventsAsync` override | `event_projection_enrichment_tests` (×3 variants) |
-| `SingleStreamProjection` subclass with Apply on the projection | `single_stream_projection_with_string_identity_tests` (StringQuestPartyProjection) |
+| `EventProjection` with `Create(TEvent)` returning a doc | `EventProjectionRegistrationCompliance` (AuditRecordCreatorProjection) |
+| `EventProjection` with `ApplyAsync` override | `EventProjectionRegistrationCompliance` (AuditRecordProjection) |
+| `EventProjection` with `EnrichEventsAsync` override | `EventProjectionEnrichmentCompliance` (×3 variants) |
+| `SingleStreamProjection` subclass with Apply on the projection | `StringIdentitySingleStreamCompliance` (StringQuestPartyProjection) |
| `MultiStreamProjection` subclass with Apply on the projection | `multi_stream_projection_tests`, `time_based_multi_stream_projection_tests` |
| `PolecatCompositeProjection` with `Snapshot` | `composite_projection_tests` |
| `PolecatCompositeProjection` with `TryFindUpstreamCache` between stages | `composite_try_find_upstream_cache_tests` |
diff --git a/src/Polecat.Tests/Compliance/ComplianceQuerySessionAlias.cs b/src/Polecat.Tests/Compliance/ComplianceQuerySessionAlias.cs
index f730b27..78d5853 100644
--- a/src/Polecat.Tests/Compliance/ComplianceQuerySessionAlias.cs
+++ b/src/Polecat.Tests/Compliance/ComplianceQuerySessionAlias.cs
@@ -8,3 +8,9 @@
// they cannot reach the pair their suite class is generic over.
global using ComplianceOperations = Polecat.IDocumentSession;
global using ComplianceEventProjection = Polecat.Projections.EventProjection;
+
+// The string-identity suite's custom projection needs Polecat's own SingleStreamProjection base, and
+// that one is generic over the identity type as well as the document, so this alias names a closed
+// generic rather than an open one.
+global using ComplianceStringPartyProjectionBase =
+ Polecat.Projections.SingleStreamProjection;
diff --git a/src/Polecat.Tests/Compliance/PolecatComplianceFixture.cs b/src/Polecat.Tests/Compliance/PolecatComplianceFixture.cs
index 3a07392..388b016 100644
--- a/src/Polecat.Tests/Compliance/PolecatComplianceFixture.cs
+++ b/src/Polecat.Tests/Compliance/PolecatComplianceFixture.cs
@@ -37,6 +37,17 @@ protected override async Task BuildStoreAsync(ComplianceStoreConfig config)
options.DaemonSettings.MaxConcurrentRebuildsPerDatabase = config.MaxConcurrentRebuildsPerDatabase;
}
+ if (config.StreamIdentity.HasValue)
+ {
+ options.Events.StreamIdentity = config.StreamIdentity.Value;
+ }
+
+ if (config.EnableCorrelationTracking)
+ {
+ options.Events.EnableCorrelationId = true;
+ options.Events.EnableCausationId = true;
+ }
+
config.ApplyTo(new PolecatComplianceRegistrar(options));
_store = new DocumentStore(options);
@@ -81,6 +92,13 @@ public override Task SaveChangesAsync(IDocumentSession session, CancellationToke
public override IEventStoreOperations EventsFor(IDocumentSession session) => session.Events;
+ public override string? CorrelationIdFor(IDocumentSession session) => session.CorrelationId;
+
+ public override string? CausationIdFor(IDocumentSession session) => session.CausationId;
+
+ public override void SetCorrelationId(IDocumentSession session, string? correlationId)
+ => session.CorrelationId = correlationId;
+
public override IEventStore EventStore => _store;
public override IEnumerable AllAggregateTypes() => _store.Options.Projections.AllAggregateTypes();
diff --git a/src/Polecat.Tests/Compliance/polecat_event_store_compliance.cs b/src/Polecat.Tests/Compliance/polecat_event_store_compliance.cs
index f4d505d..e3693f7 100644
--- a/src/Polecat.Tests/Compliance/polecat_event_store_compliance.cs
+++ b/src/Polecat.Tests/Compliance/polecat_event_store_compliance.cs
@@ -33,3 +33,9 @@ public class event_projection_enrichment_compliance
public class rebuild_concurrency_cap_compliance
: RebuildConcurrencyCapCompliance;
+
+public class activity_correlation_compliance
+ : ActivityCorrelationCompliance;
+
+public class string_identity_single_stream_compliance
+ : StringIdentitySingleStreamCompliance;
diff --git a/src/Polecat.Tests/Events/activity_correlation_tests.cs b/src/Polecat.Tests/Events/activity_correlation_tests.cs
deleted file mode 100644
index 7b40128..0000000
--- a/src/Polecat.Tests/Events/activity_correlation_tests.cs
+++ /dev/null
@@ -1,116 +0,0 @@
-using System.Diagnostics;
-using Polecat.Tests.Harness;
-
-namespace Polecat.Tests.Events;
-
-///
-/// #239: a session auto-seeds CorrelationId from Activity.Current.RootId and CausationId from
-/// Activity.Current.ParentId on open, so distributed-tracing context flows onto events with zero
-/// app code (mirrors Marten). An explicit caller value still wins.
-///
-public class activity_correlation_tests : OneOffConfigurationsContext
-{
- private async Task ConfigureAndApply(Action configure)
- {
- ConfigureStore(configure);
- await theDatabase.ApplyAllConfiguredChangesToDatabaseAsync();
- }
-
- private static (Activity parent, Activity child) StartActivityScope()
- {
- Activity.DefaultIdFormat = ActivityIdFormat.W3C;
- var parent = new Activity("parent").Start();
- var child = new Activity("child").Start(); // child of parent → has a non-null ParentId
- return (parent, child);
- }
-
- [Fact]
- public async Task session_seeds_correlation_and_causation_from_activity()
- {
- await ConfigureAndApply(_ => { });
-
- var (parent, child) = StartActivityScope();
- try
- {
- await using var session = theStore.LightweightSession();
- session.CorrelationId.ShouldBe(child.RootId);
- session.CausationId.ShouldBe(child.ParentId);
- }
- finally
- {
- child.Stop();
- parent.Stop();
- }
- }
-
- [Fact]
- public async Task events_appended_in_activity_scope_carry_root_and_parent_ids()
- {
- await ConfigureAndApply(opts =>
- {
- opts.Events.EnableCorrelationId = true;
- opts.Events.EnableCausationId = true;
- });
-
- var (parent, child) = StartActivityScope();
- var streamId = Guid.NewGuid();
- try
- {
- await using var session = theStore.LightweightSession();
- session.Events.StartStream(streamId, new QuestStarted("Traced Quest"));
- await session.SaveChangesAsync(TestContext.Current.CancellationToken);
- }
- finally
- {
- child.Stop();
- parent.Stop();
- }
-
- await using var query = theStore.QuerySession();
- var events = await query.Events.FetchStreamAsync(streamId, token: TestContext.Current.CancellationToken);
-
- events.Count.ShouldBe(1);
- events[0].CorrelationId.ShouldBe(child.RootId);
- events[0].CausationId.ShouldBe(child.ParentId);
- }
-
- [Fact]
- public async Task explicit_caller_value_wins_over_activity()
- {
- await ConfigureAndApply(_ => { });
-
- var (parent, child) = StartActivityScope();
- try
- {
- await using var session = theStore.LightweightSession();
- session.CorrelationId = "explicit-corr";
- session.CorrelationId.ShouldBe("explicit-corr");
- session.CorrelationId.ShouldNotBe(child.RootId);
- }
- finally
- {
- child.Stop();
- parent.Stop();
- }
- }
-
- [Fact]
- public async Task no_activity_leaves_correlation_null()
- {
- await ConfigureAndApply(_ => { });
-
- // Ensure no ambient activity for this test.
- var saved = Activity.Current;
- Activity.Current = null;
- try
- {
- await using var session = theStore.LightweightSession();
- session.CorrelationId.ShouldBeNull();
- session.CausationId.ShouldBeNull();
- }
- finally
- {
- Activity.Current = saved;
- }
- }
-}
diff --git a/src/Polecat.Tests/Linq/additional_linq_operator_tests.cs b/src/Polecat.Tests/Linq/additional_linq_operator_tests.cs
index 0d6894f..a62f4c6 100644
--- a/src/Polecat.Tests/Linq/additional_linq_operator_tests.cs
+++ b/src/Polecat.Tests/Linq/additional_linq_operator_tests.cs
@@ -4,9 +4,24 @@
namespace Polecat.Tests.Linq;
///
-/// Tests for LINQ operators ported from Marten: negation, modulo, Last/LastOrDefault,
-/// .Equals(), nullable types, and HasValue.
+/// ported: Marten — LINQ operators duplicated from Marten's suite: negation, modulo,
+/// Last/LastOrDefault, .Equals(), nullable types, and HasValue.
///
+///
+/// The last remaining duplication of a Marten test after compliance wave 3 (#399), and the one
+/// the shared library cannot absorb: LINQ is document-database behavior, and
+/// JasperFx.Events.ComplianceTests spans only the event sourcing surface. Polecat still
+/// re-declares IDocumentStore/IDocumentSession/IQuerySession in its own
+/// namespace, so there is nothing for a document-db compliance suite to be generic over yet;
+/// Weasel.Storage is the intended convergence layer. Tracked in marten#5118.
+///
+///
+/// Marker convention: ported: means this file duplicates a test that also exists in
+/// another store and should eventually move into a shared suite. A bare marten#NNNN
+/// citation elsewhere in this suite means the opposite — Polecat implemented the same
+/// feature and the issue number is provenance worth keeping.
+///
+///
public class additional_linq_operator_tests : OneOffConfigurationsContext
{
private async Task StoreSeedDataAsync()
diff --git a/src/Polecat.Tests/Polecat.Tests.csproj b/src/Polecat.Tests/Polecat.Tests.csproj
index 1714f2f..5cecd77 100644
--- a/src/Polecat.Tests/Polecat.Tests.csproj
+++ b/src/Polecat.Tests/Polecat.Tests.csproj
@@ -30,7 +30,14 @@
-
+
+
+
diff --git a/src/Polecat.Tests/Projections/aggregateto_linq_operator_tests.cs b/src/Polecat.Tests/Projections/aggregateto_linq_operator_tests.cs
index 6a71f30..32a0b74 100644
--- a/src/Polecat.Tests/Projections/aggregateto_linq_operator_tests.cs
+++ b/src/Polecat.Tests/Projections/aggregateto_linq_operator_tests.cs
@@ -8,8 +8,27 @@ namespace Polecat.Tests.Projections;
// #364 follow-up (Marten parity): AggregateToAsync() folds every event matched by an event query
// into a single aggregate, regardless of stream, optionally starting from supplied state. The
// aggregate's identity is stamped from the last queried event's stream. Reuses the self-aggregating
-// QuestParty (inline_projection_tests.cs) and SelfAggregatingStringQuest
-// (single_stream_projection_with_string_identity_tests.cs) declared in this namespace.
+// QuestParty (inline_projection_tests.cs) declared in this namespace.
+
+///
+/// String-keyed self-aggregating type for the key-identity case below.
+///
+///
+/// Lived in single_stream_projection_with_string_identity_tests.cs until that file was retired
+/// into JasperFx.Events.ComplianceTests (#399). The shared library's equivalent folds the
+/// compliance suite's own events, so this test keeps a local type that folds Polecat's.
+///
+public partial class AggregateToStringQuest
+{
+ public string Id { get; set; } = string.Empty;
+ public string Name { get; set; } = string.Empty;
+ public List Members { get; set; } = new();
+
+ public static AggregateToStringQuest Create(QuestStarted e) => new() { Name = e.Name };
+
+ public void Apply(MembersJoined e) => Members.AddRange(e.Members);
+}
+
public class aggregateto_linq_operator_tests : OneOffConfigurationsContext
{
private readonly MembersJoined _joined1 = new(1, "Emond's Field", ["Rand", "Matrim", "Perrin", "Thom"]);
@@ -115,7 +134,7 @@ public async Task gets_the_key_set()
var quest = await session.Events.QueryAllRawEvents()
.Where(x => x.StreamKey == key)
- .AggregateToAsync(token: TestContext.Current.CancellationToken);
+ .AggregateToAsync(token: TestContext.Current.CancellationToken);
quest.ShouldNotBeNull();
quest.Id.ShouldBe(key);
diff --git a/src/Polecat.Tests/Projections/single_stream_projection_with_string_identity_tests.cs b/src/Polecat.Tests/Projections/single_stream_projection_with_string_identity_tests.cs
deleted file mode 100644
index 0aa10bf..0000000
--- a/src/Polecat.Tests/Projections/single_stream_projection_with_string_identity_tests.cs
+++ /dev/null
@@ -1,248 +0,0 @@
-using JasperFx.Events;
-using JasperFx.Events.Projections;
-using Polecat.Projections;
-using Polecat.Tests.Harness;
-
-namespace Polecat.Tests.Projections;
-
-///
-/// Aggregate document with a string identity, for use with string-keyed streams.
-///
-public partial class StringQuestParty
-{
- public string Id { get; set; } = string.Empty;
- public string Name { get; set; } = string.Empty;
- public List Members { get; set; } = new();
- public string? Location { get; set; }
- public List MonstersSlain { get; set; } = new();
-}
-
-///
-/// Self-aggregating document type with string identity, for use with
-/// the Snapshot<T, TId> API (conventional Apply/Create methods on the type itself).
-///
-public partial class SelfAggregatingStringQuest
-{
- public string Id { get; set; } = string.Empty;
- public string Name { get; set; } = string.Empty;
- public List Members { get; set; } = new();
-
- public static SelfAggregatingStringQuest Create(QuestStarted e)
- {
- return new SelfAggregatingStringQuest { Name = e.Name };
- }
-
- public void Apply(MembersJoined e)
- {
- Members.AddRange(e.Members);
- }
-}
-
-///
-/// Custom SingleStreamProjection with TId = string, demonstrating the new
-/// two-type-parameter signature that mirrors Marten's SingleStreamProjection<TDoc, TId>.
-///
-public partial class StringQuestPartyProjection : SingleStreamProjection
-{
- public StringQuestPartyProjection()
- {
- }
-
- public static StringQuestParty Create(IEvent @event)
- {
- return new StringQuestParty
- {
- Id = @event.StreamKey!,
- Name = @event.Data.Name
- };
- }
-
- public void Apply(MembersJoined e, StringQuestParty party)
- {
- party.Members.AddRange(e.Members);
- party.Location = e.Location;
- }
-
- public void Apply(MembersDeparted e, StringQuestParty party)
- {
- foreach (var m in e.Members) party.Members.Remove(m);
- }
-
- public void Apply(ArrivedAtLocation e, StringQuestParty party)
- {
- party.Location = e.Location;
- }
-
- public void Apply(MonsterSlain e, StringQuestParty party)
- {
- party.MonstersSlain.Add(e.Name);
- }
-
- public bool ShouldDelete(QuestEnded e)
- {
- return true;
- }
-}
-
-[Collection("integration")]
-public class single_stream_projection_with_string_identity_tests : IntegrationContext
-{
- public single_stream_projection_with_string_identity_tests(DefaultStoreFixture fixture) : base(fixture)
- {
- }
-
- private async Task CreateStoreWithStringProjection()
- {
- await StoreOptions(opts =>
- {
- opts.DatabaseSchemaName = "string_proj";
- opts.Events.StreamIdentity = StreamIdentity.AsString;
- opts.Projections.Add(JasperFx.Events.Projections.ProjectionLifecycle.Inline);
- });
- return theStore;
- }
-
- [Fact]
- public async Task custom_projection_creates_aggregate_on_start_stream()
- {
- var store = await CreateStoreWithStringProjection();
-
- var streamKey = "quest-" + Guid.NewGuid();
- await using var session = store.LightweightSession();
- session.Events.StartStream(streamKey,
- new QuestStarted("Destroy the Ring"));
- await session.SaveChangesAsync(TestContext.Current.CancellationToken);
-
- await using var query = store.QuerySession();
- var party = await query.LoadAsync(streamKey, TestContext.Current.CancellationToken);
-
- party.ShouldNotBeNull();
- party.Id.ShouldBe(streamKey);
- party.Name.ShouldBe("Destroy the Ring");
- }
-
- [Fact]
- public async Task custom_projection_applies_multiple_events()
- {
- var store = await CreateStoreWithStringProjection();
-
- var streamKey = "quest-" + Guid.NewGuid();
- await using var session = store.LightweightSession();
- session.Events.StartStream(streamKey,
- new QuestStarted("Fellowship"),
- new MembersJoined(1, "Rivendell", ["Aragorn", "Legolas", "Gimli"]));
- await session.SaveChangesAsync(TestContext.Current.CancellationToken);
-
- await using var query = store.QuerySession();
- var party = await query.LoadAsync(streamKey, TestContext.Current.CancellationToken);
-
- party.ShouldNotBeNull();
- party.Name.ShouldBe("Fellowship");
- party.Members.ShouldBe(["Aragorn", "Legolas", "Gimli"]);
- party.Location.ShouldBe("Rivendell");
- }
-
- [Fact]
- public async Task custom_projection_updates_on_append()
- {
- var store = await CreateStoreWithStringProjection();
-
- var streamKey = "quest-" + Guid.NewGuid();
- await using var session = store.LightweightSession();
- session.Events.StartStream(streamKey,
- new QuestStarted("Adventure"),
- new MembersJoined(1, "Start", ["Hero"]));
- await session.SaveChangesAsync(TestContext.Current.CancellationToken);
-
- await using var session2 = store.LightweightSession();
- session2.Events.Append(streamKey,
- new ArrivedAtLocation("Dungeon", 2),
- new MonsterSlain("Goblin", 50));
- await session2.SaveChangesAsync(TestContext.Current.CancellationToken);
-
- await using var query = store.QuerySession();
- var party = await query.LoadAsync(streamKey, TestContext.Current.CancellationToken);
-
- party.ShouldNotBeNull();
- party.Location.ShouldBe("Dungeon");
- party.MonstersSlain.ShouldContain("Goblin");
- }
-
- [Fact]
- public async Task custom_projection_should_delete_removes_aggregate()
- {
- var store = await CreateStoreWithStringProjection();
-
- var streamKey = "quest-" + Guid.NewGuid();
- await using var session = store.LightweightSession();
- session.Events.StartStream(streamKey,
- new QuestStarted("Doomed Quest"),
- new MembersJoined(1, "Start", ["Hero"]));
- await session.SaveChangesAsync(TestContext.Current.CancellationToken);
-
- await using var session2 = store.LightweightSession();
- session2.Events.Append(streamKey,
- new QuestEnded("Doomed Quest"));
- await session2.SaveChangesAsync(TestContext.Current.CancellationToken);
-
- await using var query = store.QuerySession();
- var party = await query.LoadAsync(streamKey, TestContext.Current.CancellationToken);
- party.ShouldBeNull();
- }
-
- [Fact]
- public async Task multiple_string_keyed_streams_projected_independently()
- {
- var store = await CreateStoreWithStringProjection();
-
- var key1 = "quest-" + Guid.NewGuid();
- var key2 = "quest-" + Guid.NewGuid();
-
- await using var session = store.LightweightSession();
- session.Events.StartStream(key1,
- new QuestStarted("Quest 1"),
- new MembersJoined(1, "Town A", ["Alpha"]));
- session.Events.StartStream(key2,
- new QuestStarted("Quest 2"),
- new MembersJoined(1, "Town B", ["Beta", "Gamma"]));
- await session.SaveChangesAsync(TestContext.Current.CancellationToken);
-
- await using var query = store.QuerySession();
- var party1 = await query.LoadAsync(key1, TestContext.Current.CancellationToken);
- var party2 = await query.LoadAsync(key2, TestContext.Current.CancellationToken);
-
- party1.ShouldNotBeNull();
- party1.Name.ShouldBe("Quest 1");
- party1.Members.ShouldBe(["Alpha"]);
-
- party2.ShouldNotBeNull();
- party2.Name.ShouldBe("Quest 2");
- party2.Members.ShouldBe(["Beta", "Gamma"]);
- }
-
- [Fact]
- public async Task self_aggregating_projection_with_string_identity()
- {
- // Use SingleStreamProjection with a self-aggregating type
- await StoreOptions(opts =>
- {
- opts.DatabaseSchemaName = "string_snap";
- opts.Events.StreamIdentity = StreamIdentity.AsString;
- opts.Projections.Add>(ProjectionLifecycle.Inline);
- });
-
- var streamKey = "snap-" + Guid.NewGuid();
- await using var session = theStore.LightweightSession();
- session.Events.StartStream(streamKey,
- new QuestStarted("Snapshot Quest"),
- new MembersJoined(1, "Castle", ["Knight"]));
- await session.SaveChangesAsync(TestContext.Current.CancellationToken);
-
- await using var query = theStore.QuerySession();
- var party = await query.LoadAsync(streamKey, TestContext.Current.CancellationToken);
-
- party.ShouldNotBeNull();
- party.Name.ShouldBe("Snapshot Quest");
- party.Members.ShouldBe(["Knight"]);
- }
-}