diff --git a/packages/api/src/skills/sync/github.spec.ts b/packages/api/src/skills/sync/github.spec.ts index cd36c2bc9e..ff5d47f190 100644 --- a/packages/api/src/skills/sync/github.spec.ts +++ b/packages/api/src/skills/sync/github.spec.ts @@ -463,6 +463,11 @@ describe('createGitHubSkillSyncRunner', () => { expect(result.status).toBe('completed'); expect(observedTenantId).toBe('tenant-a'); + expect(deps.findSkillBySourceIdentity).toHaveBeenCalledWith({ + source: 'github', + upstreamId: 'librechat-skills:skills/research', + tenantId: 'tenant-a', + }); expect(deps.createSkill).toHaveBeenCalledWith( expect.objectContaining({ name: 'research', tenantId: 'tenant-a' }), ); @@ -778,6 +783,7 @@ describe('createGitHubSkillSyncRunner', () => { expect(deps.findSkillBySourceIdentity).toHaveBeenCalledWith({ source: 'github', upstreamId: 'librechat-skills:skills/research', + tenantId: undefined, }); expect(deps.createSkill).not.toHaveBeenCalled(); expect(deps.updateSkill).toHaveBeenCalledWith( diff --git a/packages/api/src/skills/sync/github.ts b/packages/api/src/skills/sync/github.ts index 1eda2f8009..1e326ad123 100644 --- a/packages/api/src/skills/sync/github.ts +++ b/packages/api/src/skills/sync/github.ts @@ -157,6 +157,7 @@ export type GitHubSkillSyncDeps = { findSkillBySourceIdentity: (params: { source: 'github' | 'notion'; upstreamId: string; + tenantId?: string; }) => Promise<(ISkill & { _id: Types.ObjectId }) | null>; listSkillsBySource: (params: { source: 'github' | 'notion'; @@ -803,8 +804,12 @@ async function prepareRemoteSkill(params: { source: PROVIDER, sourceMetadata, }; - const foundExisting = await deps.findSkillBySourceIdentity({ source: PROVIDER, upstreamId }); const sourceTenantId = source.tenantId ?? undefined; + const foundExisting = await deps.findSkillBySourceIdentity({ + source: PROVIDER, + upstreamId, + tenantId: sourceTenantId, + }); const existing = foundExisting && (foundExisting.tenantId ?? undefined) === sourceTenantId ? foundExisting diff --git a/packages/data-schemas/src/methods/skill.spec.ts b/packages/data-schemas/src/methods/skill.spec.ts index 85ecf41bf8..fda6b045b1 100644 --- a/packages/data-schemas/src/methods/skill.spec.ts +++ b/packages/data-schemas/src/methods/skill.spec.ts @@ -507,6 +507,48 @@ describe('Skill CRUD methods', () => { expect(await SkillFile.countDocuments({ skillId: skill._id })).toBe(0); }); + it('findSkillBySourceIdentity searches only the requested tenant bucket', async () => { + const upstreamId = 'librechat-skills:skills/research'; + const sourceMetadata = { + provider: 'github', + sourceId: 'librechat-skills', + upstreamId, + }; + const author = new mongoose.Types.ObjectId(); + const tenantSkill = await Skill.create({ + name: 'research', + description: 'A tenant-scoped GitHub skill mirror.', + body: 'tenant body', + author, + authorName: 'GitHub Sync', + tenantId: 'tenant-a', + source: 'github', + sourceMetadata, + }); + const ambientSkill = await Skill.create({ + name: 'research', + description: 'An ambient GitHub skill mirror.', + body: 'ambient body', + author, + authorName: 'GitHub Sync', + source: 'github', + sourceMetadata, + }); + + const ambientResult = await methods.findSkillBySourceIdentity({ + source: 'github', + upstreamId, + }); + const tenantResult = await methods.findSkillBySourceIdentity({ + source: 'github', + upstreamId, + tenantId: 'tenant-a', + }); + + expect(ambientResult?._id.toString()).toBe(ambientSkill._id.toString()); + expect(tenantResult?._id.toString()).toBe(tenantSkill._id.toString()); + }); + it('listSkillsByAccess returns only accessible skills and paginates by cursor', async () => { const ids: mongoose.Types.ObjectId[] = []; for (let i = 0; i < 3; i++) { diff --git a/packages/data-schemas/src/methods/skill.ts b/packages/data-schemas/src/methods/skill.ts index 998ad9e8b5..64c9fad424 100644 --- a/packages/data-schemas/src/methods/skill.ts +++ b/packages/data-schemas/src/methods/skill.ts @@ -1365,11 +1365,16 @@ export function createSkillMethods(mongoose: typeof import('mongoose'), deps: Sk async function findSkillBySourceIdentity(params: { source: 'github' | 'notion'; upstreamId: string; + tenantId?: string; }): Promise<(ISkill & { _id: Types.ObjectId }) | null> { const Skill = mongoose.models.Skill as Model; + const tenantFilter: FilterQuery = params.tenantId + ? { tenantId: params.tenantId } + : { $or: [{ tenantId: { $exists: false } }, { tenantId: null }] }; const doc = await Skill.findOne({ source: params.source, 'sourceMetadata.upstreamId': params.upstreamId, + ...tenantFilter, }).lean(); return (doc as unknown as (ISkill & { _id: Types.ObjectId }) | null) ?? null; }