fix(marketplace): preserve release sync semantics

This commit is contained in:
2026-08-29 14:17:39 +08:00
parent d04b031040
commit 227b8214a8
8 changed files with 270 additions and 14 deletions

View File

@@ -205,7 +205,10 @@ describe('effective plugin resolver', () => {
{ id: 'shared-skill', entryPath: 'skills/shared/SKILL.md', packageRoot: 'C:/packages/collision-a-1' },
]);
expect(result.pluginReleaseIds).toEqual(['collision-a-1']);
expect(result.unavailableReasons).toEqual([]);
expect(result.unavailableReasons).toEqual(expect.arrayContaining([
expect.objectContaining({ pluginId: coreCollision.id, code: 'skill_owner_conflict' }),
expect.objectContaining({ pluginId: packageB.id, code: 'skill_owner_conflict' }),
]));
});
it('reserves every bundled Skill owner, including Data Service, before accepting Marketplace packages', async () => {
@@ -238,9 +241,10 @@ describe('effective plugin resolver', () => {
});
expect(result.pluginReleaseIds).toEqual([]);
expect(result.unavailableReasons).not.toEqual([
expect.objectContaining({ pluginId: marketplaceDataServiceCollision.id }),
]);
expect(result.unavailableReasons).toContainEqual(expect.objectContaining({
pluginId: marketplaceDataServiceCollision.id,
code: 'skill_owner_conflict',
}));
});
it('requires current Library, installed Release, project selection, and assignment', async () => {
@@ -284,6 +288,40 @@ describe('effective plugin resolver', () => {
});
});
it('keeps installed skill-only local under a trusted stale Library while hosted stays fail-closed', async () => {
const staleNotesLibrary = { ...library(), stale: true };
await expect(resolve(resolver({ getLibrary: vi.fn(async () => staleNotesLibrary) })))
.resolves.toMatchObject({
effectiveSkillIds: ['notes'],
pluginReleaseIds: ['notes-1'],
unavailableReasons: [],
});
const staleHostedLibrary = {
...library(),
stale: true,
items: [{ ...library().items[0], pluginId: serverDefinition.id }],
};
const hosted = createEffectivePluginResolver({
definitions: [serverDefinition],
getAccountBinding: () => binding,
getLibrary: vi.fn(async () => staleHostedLibrary),
getInstalled: vi.fn(async () => installed(serverDefinition)),
getEnabledPluginIds: vi.fn(async () => [serverDefinition.id]),
policyClient: { getState: currentPolicy, refresh: vi.fn() },
});
await expect(hosted.resolve({
projectId: 'project-a', projectPath: 'C:/project-a', assignedSkillIds: ['remote'], role: 'parent',
})).resolves.toMatchObject({
effectiveSkillIds: [],
pluginReleaseIds: [],
unavailableReasons: [expect.objectContaining({
pluginId: serverDefinition.id,
code: 'library_unavailable',
})],
});
});
it('never exposes plugin resources to a child worker', async () => {
const result = await resolver().resolve({
projectId: 'project-a', projectPath: 'C:/project-a', assignedSkillIds: ['notes'], role: 'child',