// @vitest-environment node import { describe, expect, it, vi } from 'vitest'; import { createCodingPluginMarketplaceService } from '../../electron/api/coding-product-services'; import type { MarketplaceLibrarySnapshot } from '../../electron/coding-plugins/account-plugin-cache'; import path from 'node:path'; import { loadBundledCodingPluginDefinitions } from '../../electron/coding-plugins/manifest'; import { createEffectivePluginResolver } from '../../electron/coding-plugins/effective-resolver'; const pluginId = 'makelore.child-friendly-replies'; function fixture() { let binding = { accountKey: 'a'.repeat(64), epoch: 1 }; let library: MarketplaceLibrarySnapshot = { items: [], total: 0, stale: false, fetchedAt: 1 }; const entry = { pluginId, title: '对孩子说清楚', summary: '简明表达', category: 'communication', acquisition: 'free' as const, acquisitionMode: 'user_acquired' as const, catalogStatus: 'active' as const, runtimeStatus: 'enabled' as const, acquiredAt: '2026-09-28T00:00:00Z', removedAt: null, stableVersion: '1.0.1', betaVersion: null, }; const marketplace = { getCurrentAccountBinding: () => binding, readLibrary: vi.fn(async () => library), readDetail: vi.fn(async () => ({ pluginId, acquisition: 'free', runtimeStatus: 'enabled', stale: false, stableRelease: { deliveryKind: 'bundled', releaseId: '00000000-0000-4000-8000-000000000605', signingKeyId: null, descriptorSignature: null, minMakeloreVersion: '2.0.2', maxMakeloreVersion: null }, })), acquire: vi.fn(async () => (library = { ...library, items: [entry], total: 1 })), remove: vi.fn(async () => (library = { ...library, items: [{ ...entry, removedAt: '2026-09-28T01:00:00Z' }], total: 1, })), }; const packageStore = { getInstalled: vi.fn(async () => null), resolveAndInstall: vi.fn(), uninstall: vi.fn(async () => ({ pluginId, status: 'removed' })), }; const onChanged = vi.fn(); const service = createCodingPluginMarketplaceService({ marketplace: marketplace as never, packageStore: packageStore as never, clientVersion: '2.0.2', onChanged, }); return { service, marketplace, packageStore, onChanged, switchAccount() { binding = { accountKey: 'b'.repeat(64), epoch: 2 }; }, }; } describe('default bundled child-friendly replies', () => { it('uses the packaged Skill after project enablement, and stops using it after removal', async () => { const f = fixture(); await f.service.readLibrary(); const definitions = (await loadBundledCodingPluginDefinitions(path.resolve('resources/coding-plugins'))) .filter(({ id }) => id === pluginId); let enabled: string[] = []; const resolver = createEffectivePluginResolver({ definitions, getAccountBinding: f.marketplace.getCurrentAccountBinding, getLibrary: f.marketplace.readLibrary, getEnabledPluginIds: async () => enabled, }); const input = { projectId: 'project', projectPath: 'C:/project', assignedSkillIds: [], role: 'parent' as const }; expect((await resolver.resolve(input)).effectiveSkillIds).toEqual([]); enabled = [pluginId]; expect(await resolver.resolve(input)).toMatchObject({ pluginReleaseIds: ['00000000-0000-4000-8000-000000000605'], effectiveSkillIds: ['makelore-child-friendly-replies'], unavailableReasons: [], }); await f.service.remove(pluginId); expect((await resolver.resolve(input)).effectiveSkillIds).toEqual([]); expect(f.packageStore.resolveAndInstall).not.toHaveBeenCalled(); }); it('acquires the built-in Skill without signatures or any package download', async () => { const f = fixture(); const result = await f.service.readLibrary(); expect(result.library.items).toEqual([expect.objectContaining({ pluginId, removedAt: null })]); expect(f.packageStore.resolveAndInstall).not.toHaveBeenCalled(); expect(f.onChanged).toHaveBeenCalledWith({ pluginId, kind: 'install' }); await f.service.readLibrary(); expect(f.marketplace.acquire).toHaveBeenCalledTimes(1); }); it('respects account removal and uninstall on later refreshes', async () => { const f = fixture(); await f.service.readLibrary(); await f.service.uninstall(pluginId); expect((await f.service.readLibrary()).library.items[0].removedAt).toBeTruthy(); expect(f.marketplace.acquire).toHaveBeenCalledTimes(1); expect(f.packageStore.resolveAndInstall).not.toHaveBeenCalled(); }); it('waits for the matching bundled server release instead of downloading the old unsigned release', async () => { const f = fixture(); const detail = await f.marketplace.readDetail(); f.marketplace.readDetail.mockResolvedValueOnce({ ...detail, stableRelease: { ...detail.stableRelease, deliveryKind: 'artifact', releaseId: 'old-release' }, }); expect((await f.service.readLibrary()).library.items).toEqual([]); expect(f.marketplace.acquire).not.toHaveBeenCalled(); expect((await f.service.readLibrary()).library.items).toHaveLength(1); expect(f.packageStore.resolveAndInstall).not.toHaveBeenCalled(); }); it('retries failed acquisition on refresh', async () => { const f = fixture(); f.marketplace.acquire.mockRejectedValueOnce(new Error('backend unavailable')); expect((await f.service.readLibrary()).library.items).toEqual([]); expect((await f.service.readLibrary()).library.items).toHaveLength(1); expect(f.marketplace.acquire).toHaveBeenCalledTimes(2); }); it('does not acquire for a different account when login changes during the detail request', async () => { const f = fixture(); const original = f.marketplace.readDetail.getMockImplementation()!; f.marketplace.readDetail.mockImplementationOnce(async () => { f.switchAccount(); return original(); }); await expect(f.service.readLibrary()).rejects.toMatchObject({ code: 'marketplace_account_changed' }); expect(f.marketplace.acquire).not.toHaveBeenCalled(); }); it('keeps explicit removal during automatic acquisition as the final choice', async () => { const f = fixture(); let finish!: () => void; const paused = new Promise((resolve) => { finish = resolve; }); const acquire = f.marketplace.acquire.getMockImplementation()!; f.marketplace.acquire.mockImplementationOnce(async () => { await paused; return acquire(); }); const refreshing = f.service.readLibrary(); await vi.waitFor(() => expect(f.marketplace.acquire).toHaveBeenCalled()); const removing = f.service.remove(pluginId); finish(); await refreshing; await removing; expect((await f.service.readLibrary()).library.items[0].removedAt).toBeTruthy(); expect(f.marketplace.acquire).toHaveBeenCalledTimes(1); }); });