Skip to content

feat(access): AddToGroup durable continuation + /create-group skill#297

Merged
rbuergi merged 1 commit into
mainfrom
feat/add-to-group-continuation
Jul 6, 2026
Merged

feat(access): AddToGroup durable continuation + /create-group skill#297
rbuergi merged 1 commit into
mainfrom
feat/add-to-group-continuation

Conversation

@rbuergi

@rbuergi rbuergi commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

What

Adds the group-invite twin of the existing space-invite flow: invite someone to an access-control Group by email and have them land in the group the moment they onboard — the group-centric version of the durable EventSubscription that today only grants space access directly.

Changes (additive)

  • EventContinuationType.AddToGroup — new durable continuation (reuses TargetPath = group path). Fired by EventSubscriptionRunner on the same NodeChange(User Created) + email-match trigger as GrantSpaceAccess.
  • EventSubscriptionOps.AddToGroup — creates the GroupMembership node as a child of the group, so it lands in the group's partition schema where the permission matview's group-expansion CTE resolves it (content.member + content.groups[].group → the group's AccessAssignment).
  • GroupInviteExtensions.InviteToGroup(hub, groupPath, email, invitedBy) — static extension (stateless → no DI service, per the "static handlers" rule): existing user → add to group now; unknown → Invitation (email) + AddToGroup subscription. The group twin of SpaceInviteService.Invite.
  • /create-group skill (MeshWeaver.AI/Data/Skill) — MCP recipes for group + group-access grant + add-existing-member + invite-with-durable-onboarding, the one-partition placement rule, verification steps, and the Email:Enabled caveat (email is a no-op unless the deployment enables Graph Mail.Send).

Tests

  • EventSubscriptionRunnerTest.PendingAddToGroup_FiresWhenMatchingUserIsCreated
  • GroupInviteExtensionsTest — existing-user (adds membership now) + absent-email (schedules AddToGroup + creates Invitation).
  • All green locally. Release -warnaserror clean.

Drive-by fix

SkillNodeTypeTest now lists create-markdown in its expected skill set. It was added in 37949691c without updating the assertion, leaving that test red on main (masked at merge). This PR restores it to green.

Why

User wants a group-owned YouTube space where invited collaborators are added to the group on first sign-in via durable subscribers. The existing continuation only did direct space grants; this adds the missing group path and codifies the whole flow as a reusable skill.

🤖 Generated with Claude Code

Adds the group-invite twin of the existing space-invite flow: invite someone
to a Group by email and have them land in the group the moment they onboard.

- EventContinuationType.AddToGroup — additive durable continuation (reuses
  TargetPath = group path), fired by EventSubscriptionRunner on the same
  NodeChange(User Created) + email-match trigger as GrantSpaceAccess.
- EventSubscriptionOps.AddToGroup — creates the GroupMembership node as a child
  of the group (so it lands in the group's partition schema, where the
  permission matview's group-expansion CTE resolves it).
- GroupInviteExtensions.InviteToGroup(hub, groupPath, email, invitedBy) — static
  extension (stateless → no DI service): existing user → add now; unknown →
  Invitation (email) + AddToGroup subscription. The group twin of
  SpaceInviteService.Invite.
- /create-group skill (MeshWeaver.AI/Data/Skill): full MCP recipes for group +
  group-access grant + add-existing + invite-with-durable-onboarding, the
  one-partition placement rule, verification, and the Email:Enabled caveat.
- Tests: PendingAddToGroup runner test + GroupInviteExtensionsTest (2 cases).

Also lists create-markdown in SkillNodeTypeTest's expected set — it was added in
3794969 without updating the assertion, leaving that test red on main.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a durable “invite to access-control Group by email” flow, mirroring the existing space-invite mechanism, so that group membership is applied automatically when an invited user onboards.

Changes:

  • Introduces EventContinuationType.AddToGroup and wires it into EventSubscriptionRunner + EventSubscriptionOps to create group-scoped GroupMembership nodes durably.
  • Adds GroupInviteExtensions.InviteToGroup(...) to handle both existing-user immediate membership and absent-user durable onboarding via Invitation + EventSubscription.
  • Adds test coverage for the new continuation + invite behavior, and fixes the built-in skill assertion to include create-markdown / create-group.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
test/MeshWeaver.Graph.Test/GroupInviteExtensionsTest.cs New integration tests for InviteToGroup (existing user vs. invited email).
test/MeshWeaver.Graph.Test/EventSubscriptionRunnerTest.cs Adds coverage that AddToGroup continuations fire on matching User creation and land membership.
test/MeshWeaver.AI.Test/SkillNodeTypeTest.cs Updates expected built-in skill IDs to include create-group and create-markdown.
src/MeshWeaver.Mesh.Contract/EventSubscription.cs Adds EventContinuationType.AddToGroup and updates TargetPath documentation.
src/MeshWeaver.Graph/GroupInviteExtensions.cs New extension implementing the group invite flow (immediate add vs. durable onboarding).
src/MeshWeaver.Graph/EventSubscriptionRunner.cs Handles the new AddToGroup continuation type at execution time.
src/MeshWeaver.Graph/EventSubscriptionOps.cs Adds AddToGroup(...) helper creating {group}/{user}_Membership under the group.
src/MeshWeaver.AI/Data/Skill/create-group.md Adds /create-group skill documentation/recipes for creating groups, granting access, and inviting members durably.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +47 to +63
var meshService = hub.ServiceProvider.GetRequiredService<IMeshService>();
var accessService = hub.ServiceProvider.GetRequiredService<AccessService>();
var normalizedEmail = email.Trim();

// Look up an existing account by email (one-shot initial snapshot).
return meshService.Query<MeshNode>(MeshQueryRequest.FromQuery($"nodeType:User content.email:{normalizedEmail}"))
.Where(c => c.ChangeType == QueryChangeType.Initial)
.Select(c => c.Items)
.Take(1)
.SelectMany(users =>
{
var existing = users.FirstOrDefault(u => EmailMatches(hub, u, normalizedEmail));
return existing is not null
? EventSubscriptionOps.AddToGroup(meshService, existing.Id, groupPath)
.Select(_ => GroupInviteOutcome.Added)
: ScheduleAndInvite(meshService, accessService, groupPath, normalizedEmail, invitedBy);
});
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Test Results (shard 2)

696 tests  ±0   694 ✅ ±0   1m 50s ⏱️ +5s
  9 suites ±0     2 💤 ±0 
  9 files   ±0     0 ❌ ±0 

Results for commit 03a7de8. ± Comparison against base commit a2bb9b1.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Test Results (shard 1)

1 029 tests  ±0   1 028 ✅ ±0   3m 46s ⏱️ +19s
    8 suites ±0       1 💤 ±0 
    8 files   ±0       0 ❌ ±0 

Results for commit 03a7de8. ± Comparison against base commit a2bb9b1.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Test Results (shard 4)

   10 files  ± 0     10 suites  ±0   4m 35s ⏱️ -6s
1 338 tests  - 10  1 338 ✅  - 10  0 💤 ±0  0 ❌ ±0 
1 377 runs   - 10  1 377 ✅  - 10  0 💤 ±0  0 ❌ ±0 

Results for commit 03a7de8. ± Comparison against base commit a2bb9b1.

This pull request removes 13 and adds 3 tests. Note that renamed tests count towards both.
MeshWeaver.Persistence.Test.DataCubesDocBlocksTest ‑ EveryExecutableBlock_RendersAControl
MeshWeaver.Persistence.Test.DataCubesDocBlocksTest ‑ Page_HasTheExpectedExecutableBlocks
MeshWeaver.Persistence.Test.MeshNodeVersionSyncTest ‑ MeshNode_InitialVersion_IsZero
MeshWeaver.Persistence.Test.MeshNodeVersionSyncTest ‑ MeshNode_VersionProperty_CanBeSetAndPersisted
MeshWeaver.Persistence.Test.MeshNodeVersionSyncTest ‑ MessageHub_HasSetInitialVersionMethod
MeshWeaver.Persistence.Test.SidePanelChatKeyingTest ‑ DoesNotHide_NewSidePanelChat_WhenNavigatingToAnyThread
MeshWeaver.Persistence.Test.SidePanelChatKeyingTest ‑ DoesNotHide_WhenNavThreadIsThePanelThread_UnderADifferentRepresentation
MeshWeaver.Persistence.Test.SidePanelChatKeyingTest ‑ DoesNotHide_WhenNavigatedThreadIsTheOneOpenInTheSidePanel
MeshWeaver.Persistence.Test.SidePanelChatKeyingTest ‑ DoesNotHide_WhenPanelNotVisible_OrNavigatedNodeIsNotAThread
MeshWeaver.Persistence.Test.SidePanelChatKeyingTest ‑ Hides_WhenOpeningADifferentThreadFullScreen
…
MeshWeaver.Graph.Test.EventSubscriptionRunnerTest ‑ PendingAddToGroup_FiresWhenMatchingUserIsCreated
MeshWeaver.Graph.Test.GroupInviteExtensionsTest ‑ InviteAbsentEmail_SchedulesAddToGroupAndCreatesInvitation
MeshWeaver.Graph.Test.GroupInviteExtensionsTest ‑ InviteExistingUser_AddsMembershipImmediately

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Test Results (shard 3)

790 tests  ±0   790 ✅ ±0   3m 59s ⏱️ -1s
 10 suites ±0     0 💤 ±0 
 10 files   ±0     0 ❌ ±0 

Results for commit 03a7de8. ± Comparison against base commit a2bb9b1.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Test Results (shard 5)

764 tests   - 102   584 ✅  - 102   3m 16s ⏱️ -43s
  9 suites ±  0   180 💤 ±  0 
  9 files   ±  0     0 ❌ ±  0 

Results for commit 03a7de8. ± Comparison against base commit a2bb9b1.

This pull request removes 102 tests.
MeshWeaver.Hosting.Monolith.Test.CompileLeafStabilityTest ‑ Kickoff_FreshNodeTypes_AllSettle_NoStall
MeshWeaver.Hosting.Monolith.Test.CreatableTypesFileSystemTest ‑ FileSystem_GetChildrenOfACMEProject_ShouldIncludeTodo
MeshWeaver.Hosting.Monolith.Test.CreatableTypesFileSystemTest ‑ FileSystem_ProductLaunch_CreatableTypes_ShouldIncludeTodo
MeshWeaver.Hosting.Monolith.Test.CreatableTypesFileSystemTest ‑ FileSystem_QueryChildNodeTypes_ShouldFindTodo
MeshWeaver.Hosting.Monolith.Test.CreatableTypesFileSystemTest ‑ FileSystem_VerifyDataStructure
MeshWeaver.Hosting.Monolith.Test.CreatableTypesIntegrationTest ‑ ACME_CreatableTypes_IncludesProjectAndGlobalTypes
MeshWeaver.Hosting.Monolith.Test.CreatableTypesIntegrationTest ‑ CompleteCreateFlow_GetTypes_Create_Edit_Succeeds
MeshWeaver.Hosting.Monolith.Test.CreatableTypesIntegrationTest ‑ CreatableTypes_SortedByOrder
MeshWeaver.Hosting.Monolith.Test.CreatableTypesIntegrationTest ‑ CreatableTypes_WithExplicitConfig_OverridesAuto
MeshWeaver.Hosting.Monolith.Test.CreatableTypesIntegrationTest ‑ CreateForm_DefaultType_ShouldBeParentNodeType
…

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Test Results (shard 0)

631 tests  ±0   631 ✅ ±0   5m 7s ⏱️ -1s
 10 suites ±0     0 💤 ±0 
 10 files   ±0     0 ❌ ±0 

Results for commit 03a7de8. ± Comparison against base commit a2bb9b1.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Test Results

   56 files  ±  0     56 suites  ±0   22m 36s ⏱️ -24s
5 248 tests  - 112  5 065 ✅  - 112  183 💤 ±0  0 ❌ ±0 
5 287 runs   - 112  5 104 ✅  - 112  183 💤 ±0  0 ❌ ±0 

Results for commit 03a7de8. ± Comparison against base commit a2bb9b1.

This pull request removes 115 and adds 3 tests. Note that renamed tests count towards both.
MeshWeaver.Hosting.Monolith.Test.CompileLeafStabilityTest ‑ Kickoff_FreshNodeTypes_AllSettle_NoStall
MeshWeaver.Hosting.Monolith.Test.CreatableTypesFileSystemTest ‑ FileSystem_GetChildrenOfACMEProject_ShouldIncludeTodo
MeshWeaver.Hosting.Monolith.Test.CreatableTypesFileSystemTest ‑ FileSystem_ProductLaunch_CreatableTypes_ShouldIncludeTodo
MeshWeaver.Hosting.Monolith.Test.CreatableTypesFileSystemTest ‑ FileSystem_QueryChildNodeTypes_ShouldFindTodo
MeshWeaver.Hosting.Monolith.Test.CreatableTypesFileSystemTest ‑ FileSystem_VerifyDataStructure
MeshWeaver.Hosting.Monolith.Test.CreatableTypesIntegrationTest ‑ ACME_CreatableTypes_IncludesProjectAndGlobalTypes
MeshWeaver.Hosting.Monolith.Test.CreatableTypesIntegrationTest ‑ CompleteCreateFlow_GetTypes_Create_Edit_Succeeds
MeshWeaver.Hosting.Monolith.Test.CreatableTypesIntegrationTest ‑ CreatableTypes_SortedByOrder
MeshWeaver.Hosting.Monolith.Test.CreatableTypesIntegrationTest ‑ CreatableTypes_WithExplicitConfig_OverridesAuto
MeshWeaver.Hosting.Monolith.Test.CreatableTypesIntegrationTest ‑ CreateForm_DefaultType_ShouldBeParentNodeType
…
MeshWeaver.Graph.Test.EventSubscriptionRunnerTest ‑ PendingAddToGroup_FiresWhenMatchingUserIsCreated
MeshWeaver.Graph.Test.GroupInviteExtensionsTest ‑ InviteAbsentEmail_SchedulesAddToGroupAndCreatesInvitation
MeshWeaver.Graph.Test.GroupInviteExtensionsTest ‑ InviteExistingUser_AddsMembershipImmediately

@rbuergi rbuergi merged commit 3692e75 into main Jul 6, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants