feat(access): AddToGroup durable continuation + /create-group skill#297
Conversation
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>
There was a problem hiding this comment.
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.AddToGroupand wires it intoEventSubscriptionRunner+EventSubscriptionOpsto create group-scopedGroupMembershipnodes durably. - Adds
GroupInviteExtensions.InviteToGroup(...)to handle both existing-user immediate membership and absent-user durable onboarding viaInvitation+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.
| 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); | ||
| }); |
Test Results (shard 4) 10 files ± 0 10 suites ±0 4m 35s ⏱️ -6s 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. |
Test Results (shard 5)764 tests - 102 584 ✅ - 102 3m 16s ⏱️ -43s Results for commit 03a7de8. ± Comparison against base commit a2bb9b1. This pull request removes 102 tests. |
Test Results 56 files ± 0 56 suites ±0 22m 36s ⏱️ -24s 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. |
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
EventSubscriptionthat today only grants space access directly.Changes (additive)
EventContinuationType.AddToGroup— new durable continuation (reusesTargetPath= group path). Fired byEventSubscriptionRunneron the sameNodeChange(User Created)+ email-match trigger asGrantSpaceAccess.EventSubscriptionOps.AddToGroup— creates theGroupMembershipnode 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'sAccessAssignment).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) +AddToGroupsubscription. The group twin ofSpaceInviteService.Invite./create-groupskill (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 theEmail:Enabledcaveat (email is a no-op unless the deployment enables GraphMail.Send).Tests
EventSubscriptionRunnerTest.PendingAddToGroup_FiresWhenMatchingUserIsCreatedGroupInviteExtensionsTest— existing-user (adds membership now) + absent-email (schedulesAddToGroup+ createsInvitation).-warnaserrorclean.Drive-by fix
SkillNodeTypeTestnow listscreate-markdownin its expected skill set. It was added in37949691cwithout updating the assertion, leaving that test red onmain(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