Bundle child-friendly replies with MakeLore and acquire it by default
This commit is contained in:
@@ -39,14 +39,23 @@ describe('bundled coding plugin manifests', () => {
|
||||
'game-resource',
|
||||
'project-scaffold',
|
||||
'game-audio',
|
||||
'child-friendly-replies',
|
||||
]);
|
||||
expect(resolveBundledCodingPluginRootPaths(path.resolve('resources/coding-plugins'))).toEqual([
|
||||
PACKAGE_ROOT,
|
||||
GAME_RESOURCE_ROOT,
|
||||
PROJECT_SCAFFOLD_ROOT,
|
||||
path.resolve('resources/coding-plugins/game-audio'),
|
||||
path.resolve('resources/coding-plugins/child-friendly-replies'),
|
||||
]);
|
||||
expect(definitions).toHaveLength(4);
|
||||
expect(definitions).toHaveLength(5);
|
||||
expect(definitions[4]).toMatchObject({
|
||||
id: 'makelore.child-friendly-replies', version: '1.0.1', runtimeKind: 'skill_only',
|
||||
acquisitionMode: 'user_acquired', releaseId: '00000000-0000-4000-8000-000000000605',
|
||||
provenance: { source: 'bundled', packageRoot: 'child-friendly-replies' },
|
||||
skills: [{ id: 'makelore-child-friendly-replies', entryPath: 'skills/makelore-child-friendly-replies/SKILL.md', grants: [] }],
|
||||
tools: [],
|
||||
});
|
||||
expect(definitions[0]).toMatchObject({
|
||||
id: 'makelore.data-service',
|
||||
adapterId: 'data-service',
|
||||
@@ -163,6 +172,7 @@ describe('bundled coding plugin manifests', () => {
|
||||
path.resolve('tmp/game-resource'),
|
||||
path.resolve('tmp/project-scaffold'),
|
||||
path.resolve('tmp/game-audio'),
|
||||
path.resolve('tmp/child-friendly-replies'),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,25 +2,28 @@
|
||||
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 };
|
||||
let installed: object | null = null;
|
||||
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.0', betaVersion: null,
|
||||
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: 'artifact', signingKeyId: 'platform', descriptorSignature: 'signed',
|
||||
minMakeloreVersion: '2.0.0', maxMakeloreVersion: null },
|
||||
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 = {
|
||||
@@ -28,91 +31,103 @@ function fixture() {
|
||||
})),
|
||||
};
|
||||
const packageStore = {
|
||||
getInstalled: vi.fn(async () => installed),
|
||||
resolveAndInstall: vi.fn(async () => {
|
||||
installed = { pluginId, releaseId: 'child-1', version: '1.0.0', channel: 'stable' };
|
||||
return { ...installed, status: 'installed' };
|
||||
}),
|
||||
uninstall: vi.fn(async () => { installed = null; return { pluginId, status: 'removed' }; }),
|
||||
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.0', onChanged,
|
||||
clientVersion: '2.0.2', onChanged,
|
||||
});
|
||||
return { service, marketplace, packageStore, onChanged,
|
||||
switchAccount() { binding = { accountKey: 'b'.repeat(64), epoch: 2 }; },
|
||||
};
|
||||
}
|
||||
|
||||
describe('default child-friendly replies installation', () => {
|
||||
it('acquires and installs without a manual action, then leaves the installed package alone', async () => {
|
||||
describe('default bundled child-friendly replies', () => {
|
||||
it('uses the packaged Skill after project enablement, and stops using it after removal', async () => {
|
||||
const f = fixture();
|
||||
const result = await f.service.readLibrary();
|
||||
expect(result.installations).toEqual([{ pluginId, status: 'installed', releaseId: 'child-1', version: '1.0.0', channel: 'stable' }]);
|
||||
expect(f.marketplace.acquire).toHaveBeenCalledWith(pluginId);
|
||||
expect(f.packageStore.resolveAndInstall).toHaveBeenCalledWith({ pluginId, makeloreVersion: '2.0.0', channel: 'stable' });
|
||||
expect(f.onChanged).toHaveBeenCalledWith({ pluginId, kind: 'install' });
|
||||
await f.service.readLibrary();
|
||||
expect(f.packageStore.resolveAndInstall).toHaveBeenCalledTimes(1);
|
||||
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('respects account removal and default-plugin device uninstall on later refreshes', async () => {
|
||||
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(f.marketplace.remove).toHaveBeenCalledWith(pluginId);
|
||||
expect((await f.service.readLibrary()).installations).toEqual([]);
|
||||
expect(f.packageStore.resolveAndInstall).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not acquire an unsigned release or mistake it for installed', async () => {
|
||||
const f = fixture();
|
||||
f.marketplace.readDetail.mockImplementationOnce(async () => ({
|
||||
pluginId, acquisition: 'free', runtimeStatus: 'enabled', stale: false,
|
||||
stableRelease: { deliveryKind: 'artifact', signingKeyId: '', descriptorSignature: '',
|
||||
minMakeloreVersion: '2.0.0', maxMakeloreVersion: null },
|
||||
}));
|
||||
expect((await f.service.readLibrary()).installations).toEqual([]);
|
||||
expect(f.marketplace.acquire).not.toHaveBeenCalled();
|
||||
expect(f.packageStore.resolveAndInstall).not.toHaveBeenCalled();
|
||||
expect((await f.service.readLibrary()).installations).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('retries a failed download on refresh without duplicating acquisition', async () => {
|
||||
const f = fixture();
|
||||
f.packageStore.resolveAndInstall.mockRejectedValueOnce(new Error('download unavailable'));
|
||||
expect((await f.service.readLibrary()).installations).toEqual([]);
|
||||
expect((await f.service.readLibrary()).installations).toHaveLength(1);
|
||||
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();
|
||||
});
|
||||
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();
|
||||
expect(f.packageStore.resolveAndInstall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps an explicit removal made during automatic installation as the final choice', async () => {
|
||||
it('keeps explicit removal during automatic acquisition as the final choice', async () => {
|
||||
const f = fixture();
|
||||
let finish!: () => void;
|
||||
const paused = new Promise<void>((resolve) => { finish = resolve; });
|
||||
const install = f.packageStore.resolveAndInstall.getMockImplementation()!;
|
||||
f.packageStore.resolveAndInstall.mockImplementationOnce(async () => { await paused; return install(); });
|
||||
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.packageStore.resolveAndInstall).toHaveBeenCalled());
|
||||
await vi.waitFor(() => expect(f.marketplace.acquire).toHaveBeenCalled());
|
||||
const removing = f.service.remove(pluginId);
|
||||
finish();
|
||||
await refreshing;
|
||||
await removing;
|
||||
const result = await f.service.readLibrary();
|
||||
expect(result.library.items[0].removedAt).toBeTruthy();
|
||||
expect(f.packageStore.resolveAndInstall).toHaveBeenCalledTimes(1);
|
||||
expect((await f.service.readLibrary()).library.items[0].removedAt).toBeTruthy();
|
||||
expect(f.marketplace.acquire).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -303,6 +303,7 @@ describe('buildPluginWorkspaceProjection', () => {
|
||||
for (const [pluginId, title, runtimeKind] of [
|
||||
['makelore.game-resource', 'Game Resource', 'platform_hosted'],
|
||||
['makelore.project-scaffold', 'Project Scaffold', 'skill_only'],
|
||||
['makelore.child-friendly-replies', '对孩子说清楚', 'skill_only'],
|
||||
] as const) {
|
||||
const bundledCatalog = {
|
||||
...catalog.items[0]!,
|
||||
|
||||
Reference in New Issue
Block a user