feat(plugins): merge Skill management into plugins
This commit is contained in:
@@ -4,8 +4,8 @@ import path from 'node:path';
|
||||
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
|
||||
import { createCodingProjectConfigV2 } from '../../electron/coding-projects/project-config';
|
||||
|
||||
test.describe('Project configuration skills', () => {
|
||||
test('does not expose the removed Superpowers feature and supports Skill details', async ({ launchElectronApp }) => {
|
||||
test.describe('Project configuration plugin capabilities', () => {
|
||||
test('merges Skill management into the plugin tabs and removes standalone local Skills', async ({ launchElectronApp }) => {
|
||||
const parentPath = await mkdtemp(path.join(tmpdir(), 'makelore-project-configuration-e2e-'));
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
|
||||
@@ -52,7 +52,7 @@ test.describe('Project configuration skills', () => {
|
||||
const rect = card.getBoundingClientRect();
|
||||
return { left: rect.left, top: rect.top, width: rect.width };
|
||||
}));
|
||||
expect(resourceCardGeometry).toHaveLength(4);
|
||||
expect(resourceCardGeometry).toHaveLength(3);
|
||||
expect(Math.max(...resourceCardGeometry.map(({ top }) => top)) - Math.min(...resourceCardGeometry.map(({ top }) => top))).toBeLessThanOrEqual(1);
|
||||
expect(resourceCardGeometry.every(({ width }) => width > 0)).toBe(true);
|
||||
expect(resourceCardGeometry.map(({ left }) => left)).toEqual([...resourceCardGeometry.map(({ left }) => left)].sort((a, b) => a - b));
|
||||
@@ -60,7 +60,6 @@ test.describe('Project configuration skills', () => {
|
||||
const drawerWidths: number[] = [];
|
||||
for (const [resourceId, title] of [
|
||||
['resource-card-models', '可用模型'],
|
||||
['resource-card-skills', '可用技能'],
|
||||
['resource-card-knowledge', '知识文件'],
|
||||
['resource-card-plugins', '插件'],
|
||||
] as const) {
|
||||
@@ -77,7 +76,9 @@ test.describe('Project configuration skills', () => {
|
||||
if (resourceId === 'resource-card-plugins') {
|
||||
await expect(page.getByTestId('project-plugins-sheet')).toBeVisible();
|
||||
await expect(drawer.getByTestId('plugins-page')).toBeVisible();
|
||||
await expect(drawer.getByText('为你的智能体添加插件,拓展更多能力。')).toBeVisible();
|
||||
await expect(drawer.getByText('统一管理云端下发的插件与 Skill,以及本机安装的能力。')).toBeVisible();
|
||||
await expect(drawer.getByRole('tab', { name: /可选/ })).toBeVisible();
|
||||
await expect(drawer.getByRole('tab', { name: /已安装/ })).toBeVisible();
|
||||
await expect(drawer.getByRole('button', { name: /刷新/ })).toHaveCount(0);
|
||||
await expect(drawer.getByRole('searchbox')).toHaveCount(0);
|
||||
await expect(drawer.getByRole('combobox')).toHaveCount(0);
|
||||
@@ -120,7 +121,7 @@ test.describe('Project configuration skills', () => {
|
||||
enabled: true,
|
||||
model: { accountId: 'moonshot', modelId: 'kimi-k2.6', thinkingLevel: 'off' },
|
||||
modelResolution: 'resolved',
|
||||
skillIds: [],
|
||||
skillIds: ['agent-browser', 'grilling'],
|
||||
responsibility: { mission: '验证智能体编辑弹窗。', owns: [], boundaries: [], collaborators: [], principles: [] },
|
||||
prompt: '',
|
||||
archivedAt: null,
|
||||
@@ -135,18 +136,12 @@ test.describe('Project configuration skills', () => {
|
||||
await expect(page.getByTestId('legacy-project-identity-card')).toHaveCount(0);
|
||||
await expect(page.getByText(/项目 ID|项目身份|独立副本/u)).toHaveCount(0);
|
||||
|
||||
await page.getByTestId('resource-card-skills').click();
|
||||
await expect(page.getByTestId('resource-card-skills')).toHaveCount(0);
|
||||
await expect(page.getByTestId('project-agent-e2e-agent')).toContainText('0 项插件能力');
|
||||
await expect(page.getByTestId('superpowers-card')).toHaveCount(0);
|
||||
await expect(page.getByRole('switch', { name: '启用 Superpowers' })).toHaveCount(0);
|
||||
await expect(page.getByTestId('skill-card-agent-browser')).toBeVisible();
|
||||
await page.getByTestId('skill-card-agent-browser').click();
|
||||
await expect(page.getByTestId('skill-detail-view')).toBeVisible();
|
||||
await expect(page.getByTestId('skill-structure')).toContainText('SKILL.md');
|
||||
await expect(page.getByTestId('skill-main-file')).toContainText('# Makelore 开发浏览器');
|
||||
await page.getByRole('button', { name: '返回技能列表' }).locator('svg').click();
|
||||
await expect(page.getByTestId('skill-card-agent-browser')).toBeVisible();
|
||||
await expect(page.getByTestId('skill-card-agent-browser')).toHaveCount(0);
|
||||
await expect(page.getByTestId('skill-card-game-engine')).toHaveCount(0);
|
||||
await page.getByRole('button', { name: '关闭' }).click();
|
||||
await expect(page.getByTestId('resource-card-publish')).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: '一键提交审核' })).toBeVisible();
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('AgentCreationDialog plugin Skill availability', () => {
|
||||
onUpdate={onUpdate}
|
||||
/>);
|
||||
|
||||
const checkbox = screen.getByRole('checkbox', { name: '绑定技能:Data Service' });
|
||||
const checkbox = screen.getByRole('checkbox', { name: '绑定插件能力:Data Service' });
|
||||
expect(checkbox).toBeChecked();
|
||||
expect(checkbox).toBeDisabled();
|
||||
expect(screen.getByText('插件未启用;现有分配会保留,但当前不可用。')).toBeVisible();
|
||||
|
||||
@@ -134,7 +134,7 @@ describe('CodingCapabilityRegistry', () => {
|
||||
expect(unavailable.tools).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not wait for policy refresh when child or parent core-only resources cannot consume plugins', async () => {
|
||||
it('does not wait for policy refresh when child or legacy standalone assignments cannot consume plugins', async () => {
|
||||
const refresh = vi.fn(() => new Promise<void>(() => {}));
|
||||
const policyClient = { getState: () => policy, refresh };
|
||||
const child = await registry({ policyClient }).resolveWorkerResources({
|
||||
@@ -149,7 +149,7 @@ describe('CodingCapabilityRegistry', () => {
|
||||
});
|
||||
|
||||
expect(child.tools).toEqual([]);
|
||||
expect(coreOnly.effectiveSkillIds).toEqual(['grilling']);
|
||||
expect(coreOnly.effectiveSkillIds).toEqual([]);
|
||||
expect(refresh).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ async function resolve(effective: EffectivePluginResolver, assignedSkillIds = ['
|
||||
}
|
||||
|
||||
describe('effective plugin resolver', () => {
|
||||
it('rejects Marketplace Skill ID collisions with core and other packages without changing project assignment', async () => {
|
||||
it('accepts former local Skill IDs and rejects collisions between Marketplace packages', async () => {
|
||||
const coreCollision: CodingPluginDefinition = {
|
||||
...skillOnlyDefinition,
|
||||
id: 'makelore.core-collision',
|
||||
@@ -219,14 +219,20 @@ describe('effective plugin resolver', () => {
|
||||
|
||||
expect(result.effectiveSkillIds).toEqual(['agent-browser', 'shared-skill']);
|
||||
expect(result.skillEntries).toEqual([
|
||||
{ id: 'agent-browser', entryPath: 'agent-browser/SKILL.md' },
|
||||
{
|
||||
id: 'agent-browser',
|
||||
entryPath: 'skills/agent-browser/SKILL.md',
|
||||
packageRoot: 'C:/packages/core-collision-1',
|
||||
},
|
||||
{ id: 'shared-skill', entryPath: 'skills/shared/SKILL.md', packageRoot: 'C:/packages/collision-a-1' },
|
||||
]);
|
||||
expect(result.pluginReleaseIds).toEqual(['collision-a-1']);
|
||||
expect(result.pluginReleaseIds).toEqual(['core-collision-1', 'collision-a-1']);
|
||||
expect(result.unavailableReasons).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ pluginId: coreCollision.id, code: 'skill_owner_conflict' }),
|
||||
expect.objectContaining({ pluginId: packageB.id, code: 'skill_owner_conflict' }),
|
||||
]));
|
||||
expect(result.unavailableReasons).not.toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ pluginId: coreCollision.id, code: 'skill_owner_conflict' }),
|
||||
]));
|
||||
});
|
||||
|
||||
it('reserves every bundled Skill owner, including Data Service, before accepting Marketplace packages', async () => {
|
||||
|
||||
@@ -114,7 +114,7 @@ async function gitOutput(root: string, ...args: string[]): Promise<string> {
|
||||
}
|
||||
|
||||
describe('PI-105 product Host composition', () => {
|
||||
it('projects managed skills and a stable command catalog without raw Pi fields', async () => {
|
||||
it('ignores legacy standalone Skill assignments and projects a stable command catalog', async () => {
|
||||
const root = await configuredProject();
|
||||
const tools = productTools(root);
|
||||
const host = createCodingProductHost({
|
||||
@@ -131,15 +131,13 @@ describe('PI-105 product Host composition', () => {
|
||||
});
|
||||
|
||||
const skills = await host.listSkills('builder');
|
||||
expect(skills.filter(({ selected }) => selected).map(({ id }) => id)).toEqual([
|
||||
'agent-browser', 'grilling',
|
||||
]);
|
||||
expect(skills.filter(({ selected }) => selected).map(({ id }) => id)).toEqual([]);
|
||||
const commands = await host.listCommands(conversationId);
|
||||
expect(commands.slice(0, 5).map(({ source }) => source)).toEqual([
|
||||
'makelore', 'makelore', 'makelore', 'makelore', 'makelore',
|
||||
]);
|
||||
expect(commands).toContainEqual(expect.objectContaining({ name: 'custom', source: 'pi' }));
|
||||
expect(commands).toContainEqual(expect.objectContaining({ name: 'agent-browser', source: 'skill' }));
|
||||
expect(commands).not.toContainEqual(expect.objectContaining({ name: 'agent-browser', source: 'skill' }));
|
||||
expect(commands.filter(({ name }) => name === 'compact')).toHaveLength(1);
|
||||
const serialized = JSON.stringify({ skills, commands });
|
||||
expect(serialized).not.toContain(root);
|
||||
|
||||
@@ -71,43 +71,11 @@ describe('Makelore character scene partner profiles', () => {
|
||||
expect(hostApiFetchMock.mock.calls.some(([path]) => String(path).includes('/api/coding/agents'))).toBe(false);
|
||||
});
|
||||
|
||||
it('opens an installed Skill detail with its structure and main file', async () => {
|
||||
const project = {
|
||||
id: 'project-1',
|
||||
path: 'D:/projects/project-1',
|
||||
name: '详情测试项目',
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
updatedAt: '2026-01-01T00:00:00.000Z',
|
||||
lastOpenedAt: '2026-01-01T00:00:00.000Z',
|
||||
};
|
||||
codingWorkspaceStore.setState({ activeProject: project, projects: [project] });
|
||||
hostApiFetchMock.mockResolvedValue({
|
||||
skills: [{
|
||||
id: 'agent-browser',
|
||||
name: 'agent-browser',
|
||||
description: 'E2E browser debugging skill.',
|
||||
location: 'D:/managed/skills/agent-browser/SKILL.md',
|
||||
content: '# Browser debugging',
|
||||
entries: [
|
||||
{ path: 'references', type: 'directory' },
|
||||
{ path: 'references/guide.md', type: 'file' },
|
||||
{ path: 'SKILL.md', type: 'file' },
|
||||
],
|
||||
}],
|
||||
});
|
||||
|
||||
it('routes Skill management through the merged plugin module', () => {
|
||||
render(<MemoryRouter><CharacterScene /></MemoryRouter>);
|
||||
|
||||
fireEvent.click(screen.getByTestId('resource-card-skills'));
|
||||
await screen.findByText('本地已安装');
|
||||
fireEvent.click(screen.getByTestId('skill-library-card-agent-browser'));
|
||||
|
||||
expect(await screen.findByTestId('skill-detail-view')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('skill-structure')).toHaveTextContent('references/guide.md');
|
||||
expect(screen.getByTestId('skill-main-file')).toHaveTextContent('# Browser debugging');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '返回技能列表' }));
|
||||
expect(await screen.findByTestId('skill-library-card-agent-browser')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('resource-card-plugins')).toHaveTextContent('可选与已安装');
|
||||
expect(screen.queryByTestId('resource-card-skills')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('defers the automatic model refresh and applies it only after an explicit click', async () => {
|
||||
|
||||
@@ -464,7 +464,7 @@ describe('Makelore Pi extension bundle', () => {
|
||||
const worker = await host.registerWorker({
|
||||
conversationId: 'conversation-tools', generation: 1, projectId: 'project-a',
|
||||
projectPath: root,
|
||||
skillEntries: [{ id: 'agent-browser', entryPath: 'agent-browser/SKILL.md' }],
|
||||
skillEntries: [],
|
||||
catalogRevision: 3,
|
||||
extensionsDir: root,
|
||||
});
|
||||
@@ -507,7 +507,7 @@ describe('Makelore Pi extension bundle', () => {
|
||||
)).resolves.toMatchObject({
|
||||
details: {
|
||||
schema: 'runtime-context.v1',
|
||||
skills: expect.arrayContaining([expect.objectContaining({ id: 'agent-browser', selected: true })]),
|
||||
skills: [],
|
||||
},
|
||||
});
|
||||
const browserResult = await tools.get('agent_browser')?.execute?.(
|
||||
|
||||
@@ -183,8 +183,8 @@ describe('managed Pi worker opener', () => {
|
||||
accountSessionId: 'account-a\u00001',
|
||||
projectId: 'project-a',
|
||||
pluginReleaseIds: Object.freeze(['plugin-release-a']),
|
||||
effectiveSkillIds: Object.freeze(['grilling']),
|
||||
skillEntries: Object.freeze([{ id: 'grilling', entryPath: 'grilling/SKILL.md' }]),
|
||||
effectiveSkillIds: Object.freeze([]),
|
||||
skillEntries: Object.freeze([]),
|
||||
toolDefinitions: Object.freeze(DATA_SERVICE_PLUGIN_DEFINITION.tools),
|
||||
runtimePolicies: Object.freeze([]),
|
||||
unavailableReasons: Object.freeze([]),
|
||||
@@ -193,8 +193,8 @@ describe('managed Pi worker opener', () => {
|
||||
resolveWorkerResources: vi.fn(async () => ({
|
||||
catalogRevision: 1,
|
||||
pluginIds: [],
|
||||
effectiveSkillIds: ['grilling'],
|
||||
skillEntries: [{ id: 'grilling', entryPath: 'grilling/SKILL.md' }],
|
||||
effectiveSkillIds: [],
|
||||
skillEntries: [],
|
||||
tools: DATA_SERVICE_PLUGIN_DEFINITION.tools,
|
||||
effectiveSnapshot,
|
||||
})),
|
||||
@@ -286,7 +286,7 @@ describe('managed Pi worker opener', () => {
|
||||
for (const options of processOptions) {
|
||||
const argv = JSON.stringify(options.additionalArgs);
|
||||
expect(argv).toContain('--system-prompt');
|
||||
expect(argv).toContain('grilling');
|
||||
expect(argv).not.toContain('grilling');
|
||||
expect(argv).toContain('--session-id');
|
||||
expect(argv).toContain('--extension');
|
||||
expect(argv).toContain('makelore-runtime-v6.mjs');
|
||||
|
||||
@@ -93,7 +93,6 @@ async function bundledResourceFixture({ schemaVersion = 1, includeSdkAssets = tr
|
||||
};
|
||||
await mkdir(path.join(pluginRoot, 'com.makelore'), { recursive: true });
|
||||
await mkdir(path.join(pluginRoot, 'skills', 'example'), { recursive: true });
|
||||
await mkdir(path.join(sourceResources, 'coding-skills', 'agent-browser'), { recursive: true });
|
||||
await writeFile(path.join(pluginRoot, 'plugin.json'), JSON.stringify({
|
||||
name: 'example.plugin',
|
||||
version: '1.0.0',
|
||||
@@ -106,7 +105,6 @@ async function bundledResourceFixture({ schemaVersion = 1, includeSdkAssets = tr
|
||||
await writeFile(path.join(pluginRoot, 'skills', 'example', 'assets', 'sdk.ts'), 'export {};\n');
|
||||
await writeFile(path.join(pluginRoot, 'skills', 'example', 'assets', 'sdk.js'), 'export {};\n');
|
||||
}
|
||||
await writeFile(path.join(sourceResources, 'coding-skills', 'agent-browser', 'SKILL.md'), '# Browser\n');
|
||||
const resourcesDirectory = path.join(root, 'packaged');
|
||||
await cp(sourceResources, path.join(resourcesDirectory, 'resources'), { recursive: true });
|
||||
return {
|
||||
@@ -192,7 +190,7 @@ describe('final Pi product artifact verification', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('proves bundled plugin manifests, Skills, SDK assets, catalog, and core resources', async () => {
|
||||
it('proves bundled plugin manifests, Skills, SDK assets, and the absence of standalone Skill resources', async () => {
|
||||
const fixture = await bundledResourceFixture();
|
||||
await expect(verifyBundledCodingPluginResources(fixture)).resolves.toMatchObject({
|
||||
root: 'resources/coding-plugins',
|
||||
@@ -209,11 +207,27 @@ describe('final Pi product artifact verification', () => {
|
||||
tools: ['example_read'],
|
||||
}],
|
||||
catalog: { adapters: ['example-adapter'], tools: ['example_read'], packagedInAsar: true },
|
||||
coreResources: { root: 'resources/coding-skills', skills: ['agent-browser'] },
|
||||
coreResources: { root: null, files: [], skills: [] },
|
||||
result: 'pass',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects any source or packaged standalone Skill directory', async () => {
|
||||
const sourceFixture = await bundledResourceFixture();
|
||||
await mkdir(path.join(sourceFixture.projectRoot, 'resources', 'coding-skills', 'legacy'), {
|
||||
recursive: true,
|
||||
});
|
||||
await expect(verifyBundledCodingPluginResources(sourceFixture))
|
||||
.rejects.toThrow('Standalone coding Skill resources must not be bundled');
|
||||
|
||||
const packagedFixture = await bundledResourceFixture();
|
||||
await mkdir(path.join(packagedFixture.resourcesDirectory, 'resources', 'coding-skills'), {
|
||||
recursive: true,
|
||||
});
|
||||
await expect(verifyBundledCodingPluginResources(packagedFixture))
|
||||
.rejects.toThrow('Standalone coding Skill resources must not be bundled');
|
||||
});
|
||||
|
||||
it('accepts schema-2 hosted plugins whose implementation has no client SDK assets', async () => {
|
||||
const fixture = await bundledResourceFixture({ schemaVersion: 2, includeSdkAssets: false });
|
||||
await expect(verifyBundledCodingPluginResources(fixture)).resolves.toMatchObject({
|
||||
|
||||
@@ -201,27 +201,32 @@ describe('PI-090 product tools', () => {
|
||||
)).rejects.toThrow('project-relative');
|
||||
});
|
||||
|
||||
it('projects only bundled selected skills and safe command metadata', async () => {
|
||||
it('projects only Plugin-delivered selected Skills and safe command metadata', async () => {
|
||||
const pluginRoot = path.resolve('resources/coding-plugins/data-service');
|
||||
const skills = await listProductCodingSkills(
|
||||
path.resolve('resources/coding-skills'),
|
||||
['agent-browser', 'grilling'],
|
||||
['data-service'],
|
||||
[{
|
||||
id: 'data-service',
|
||||
pluginId: 'makelore.data-service',
|
||||
directory: pluginRoot,
|
||||
entryPath: 'skills/data-service/SKILL.md',
|
||||
}],
|
||||
);
|
||||
expect(skills.filter(({ selected }) => selected).map(({ id }) => id)).toEqual([
|
||||
'agent-browser', 'grilling',
|
||||
]);
|
||||
expect(skills[0]?.location).toBe('resources/coding-skills/agent-browser');
|
||||
expect(JSON.stringify(skills)).not.toContain(path.resolve('resources/coding-skills'));
|
||||
expect(skills.filter(({ selected }) => selected).map(({ id }) => id)).toEqual(['data-service']);
|
||||
expect(skills[0]?.location).toBe(pluginRoot);
|
||||
expect(JSON.stringify(skills)).not.toContain('resources/coding-skills');
|
||||
const commands = buildProductCodingCommandCatalog(skills, [
|
||||
{ name: 'custom', description: 'Custom Pi command' },
|
||||
{ name: 'compact', description: 'Must not shadow Makelore' },
|
||||
]);
|
||||
expect(commands).toContainEqual(expect.objectContaining({ name: 'compact', source: 'makelore' }));
|
||||
expect(commands).toContainEqual(expect.objectContaining({ name: 'custom', source: 'pi' }));
|
||||
expect(commands).toContainEqual(expect.objectContaining({ name: 'agent-browser', source: 'skill' }));
|
||||
expect(commands).toContainEqual(expect.objectContaining({ name: 'data-service', source: 'skill' }));
|
||||
expect(commands.some(({ name }) => name === 'planning-with-files')).toBe(false);
|
||||
await expect(listProductCodingSkills(
|
||||
path.resolve('resources/coding-skills'), ['not-installed'],
|
||||
)).rejects.toThrow('Unknown bundled coding skill');
|
||||
)).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('hides unassigned disabled plugin Skills and retains assigned ones as ineffective', async () => {
|
||||
|
||||
@@ -62,7 +62,7 @@ async function fixtureRoot(): Promise<{
|
||||
}
|
||||
|
||||
describe('Pi managed resource loader', () => {
|
||||
it('materializes only managed prompt and explicitly selected bundled skills', async () => {
|
||||
it('materializes only managed prompt and explicitly paired package Skills', async () => {
|
||||
const fixture = await fixtureRoot();
|
||||
const prompt = 'PRIVATE PARTNER PROMPT CONTENT';
|
||||
const effectiveSnapshot: EffectivePluginSnapshot = {
|
||||
@@ -70,7 +70,9 @@ describe('Pi managed resource loader', () => {
|
||||
projectId: 'project-1',
|
||||
pluginReleaseIds: ['release-a'],
|
||||
effectiveSkillIds: ['grilling'],
|
||||
skillEntries: [{ id: 'grilling', entryPath: 'grilling/SKILL.md' }],
|
||||
skillEntries: [{
|
||||
id: 'grilling', entryPath: 'grilling/SKILL.md', packageRoot: fixture.skillsDir,
|
||||
}],
|
||||
toolDefinitions: [],
|
||||
runtimePolicies: [],
|
||||
unavailableReasons: [],
|
||||
@@ -81,8 +83,8 @@ describe('Pi managed resource loader', () => {
|
||||
agentId: 'agent-1',
|
||||
prompt,
|
||||
skillEntries: [
|
||||
{ id: 'grilling', entryPath: 'grilling/SKILL.md' },
|
||||
{ id: 'grilling', entryPath: 'grilling/SKILL.md' },
|
||||
{ id: 'grilling', entryPath: 'grilling/SKILL.md', packageRoot: fixture.skillsDir },
|
||||
{ id: 'grilling', entryPath: 'grilling/SKILL.md', packageRoot: fixture.skillsDir },
|
||||
],
|
||||
catalogRevision: 11,
|
||||
bundledSkillsDir: fixture.skillsDir,
|
||||
@@ -96,7 +98,7 @@ describe('Pi managed resource loader', () => {
|
||||
expect(MAKELORE_DEFAULT_LANGUAGE_PROMPT).toContain('可见的思考过程');
|
||||
expect(resources.skillIds).toEqual(['grilling']);
|
||||
expect(resources.skillEntries).toEqual([
|
||||
{ id: 'grilling', entryPath: 'grilling/SKILL.md' },
|
||||
{ id: 'grilling', entryPath: 'grilling/SKILL.md', packageRoot: fixture.skillsDir },
|
||||
]);
|
||||
expect(resources.catalogRevision).toBe(11);
|
||||
expect(resources.skillPaths).toEqual([path.join(fixture.skillsDir, 'grilling', 'SKILL.md')]);
|
||||
@@ -109,7 +111,9 @@ describe('Pi managed resource loader', () => {
|
||||
promptFile: 'agent-1.md',
|
||||
languagePromptFile: 'agent-1.language.md',
|
||||
skillIds: ['grilling'],
|
||||
skillEntries: [{ id: 'grilling', entryPath: 'grilling/SKILL.md' }],
|
||||
skillEntries: [{
|
||||
id: 'grilling', entryPath: 'grilling/SKILL.md', packageRoot: fixture.skillsDir,
|
||||
}],
|
||||
catalogRevision: 11,
|
||||
effectivePluginSnapshot: effectiveSnapshot,
|
||||
revision: { provider: 3, resources: 7 },
|
||||
@@ -129,7 +133,9 @@ describe('Pi managed resource loader', () => {
|
||||
projectId: 'project-1',
|
||||
agentId: 'agent-1',
|
||||
prompt,
|
||||
skillEntries: [{ id: 'agent-browser', entryPath: 'agent-browser/SKILL.md' }],
|
||||
skillEntries: [{
|
||||
id: 'agent-browser', entryPath: 'agent-browser/SKILL.md', packageRoot: fixture.skillsDir,
|
||||
}],
|
||||
catalogRevision: 12,
|
||||
bundledSkillsDir: fixture.skillsDir,
|
||||
revision: { provider: 1, resources: 1 },
|
||||
@@ -183,6 +189,14 @@ describe('Pi managed resource loader', () => {
|
||||
expect(resources.catalogRevision).toBe(13);
|
||||
});
|
||||
|
||||
it('never resolves a Skill from the retired standalone resource root', async () => {
|
||||
const fixture = await fixtureRoot();
|
||||
|
||||
await expect(resolveExplicitCodingSkillPaths(fixture.skillsDir, [
|
||||
{ id: 'grilling', entryPath: 'grilling/SKILL.md' },
|
||||
])).rejects.toThrow('Plugin Skill entry is not a file');
|
||||
});
|
||||
|
||||
it('keeps each effective Skill paired with its verified package root when relative paths collide', async () => {
|
||||
const fixture = await fixtureRoot();
|
||||
const firstRoot = path.join(fixture.root, 'packages', 'first');
|
||||
@@ -210,7 +224,7 @@ describe('Pi managed resource loader', () => {
|
||||
await expect(readFile(resolved.skillPaths[1]!, 'utf8')).resolves.toBe('second package');
|
||||
});
|
||||
|
||||
it('filters a known disabled plugin Skill for the next worker and restores it after re-enable', async () => {
|
||||
it('ignores a legacy standalone assignment and restores a disabled plugin Skill after re-enable', async () => {
|
||||
const fixture = await fixtureRoot();
|
||||
const assignedSkillIds = ['grilling', 'data-service'];
|
||||
let enabled = true;
|
||||
@@ -260,13 +274,13 @@ describe('Pi managed resource loader', () => {
|
||||
});
|
||||
|
||||
const first = await resolve();
|
||||
expect(first.effectiveSkillIds).toEqual(assignedSkillIds);
|
||||
expect(first.effectiveSkillIds).toEqual(['data-service']);
|
||||
expect(first.tools).toHaveLength(10);
|
||||
|
||||
enabled = false;
|
||||
const disabled = await resolve();
|
||||
expect(assignedSkillIds).toEqual(['grilling', 'data-service']);
|
||||
expect(disabled.effectiveSkillIds).toEqual(['grilling']);
|
||||
expect(disabled.effectiveSkillIds).toEqual([]);
|
||||
expect(disabled.tools).toEqual([]);
|
||||
await expect(materializePiAgentResources({
|
||||
userDataDir: fixture.userDataDir,
|
||||
@@ -277,11 +291,11 @@ describe('Pi managed resource loader', () => {
|
||||
catalogRevision: disabled.catalogRevision,
|
||||
bundledSkillsDir: fixture.skillsDir,
|
||||
revision: { provider: 1, resources: 2 },
|
||||
})).resolves.toMatchObject({ skillIds: ['grilling'], catalogRevision: 17 });
|
||||
})).resolves.toMatchObject({ skillIds: [], catalogRevision: 17 });
|
||||
|
||||
enabled = true;
|
||||
const restored = await resolve();
|
||||
expect(restored.effectiveSkillIds).toEqual(assignedSkillIds);
|
||||
expect(restored.effectiveSkillIds).toEqual(['data-service']);
|
||||
expect(restored.tools).toHaveLength(10);
|
||||
|
||||
});
|
||||
@@ -307,7 +321,7 @@ describe('Pi managed resource loader', () => {
|
||||
const fixture = await fixtureRoot();
|
||||
await expect(resolveExplicitCodingSkillPaths(fixture.skillsDir, [
|
||||
{ id: 'not-bundled', entryPath: 'not-bundled/SKILL.md' },
|
||||
])).rejects.toThrow('Bundled coding Skill entry is not a file');
|
||||
])).rejects.toThrow('Plugin Skill entry is not a file');
|
||||
await expect(materializePiAgentResources({
|
||||
userDataDir: fixture.userDataDir,
|
||||
projectId: '../outside',
|
||||
|
||||
@@ -137,7 +137,7 @@ describe('managed Pi subagent child opener', () => {
|
||||
for (const options of processOptions) {
|
||||
expect(options.additionalArgs).toContain('--no-session');
|
||||
expect(options.additionalArgs).not.toContain('--session-id');
|
||||
expect(options.additionalArgs?.some((argument) => argument.includes('grilling'))).toBe(true);
|
||||
expect(options.additionalArgs?.some((argument) => argument.includes('grilling'))).toBe(false);
|
||||
expect(JSON.stringify(options.additionalArgs)).not.toContain('PRIVATE CHILD PROMPT');
|
||||
expect(JSON.stringify(options.additionalArgs)).not.toContain('provider-secret');
|
||||
expect(Object.values(options.env ?? {})).toContain('provider-secret');
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('planning-with-files Skill', () => {
|
||||
it('requires planning files directly in the current project root', async () => {
|
||||
const skill = await readFile(
|
||||
path.join(process.cwd(), 'resources', 'coding-skills', 'planning-with-files', 'SKILL.md'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
expect(skill).toContain('<current-project-root>/task_plan.md');
|
||||
expect(skill).toContain('不是本 Skill 的安装目录、进程工作目录、用户主目录');
|
||||
expect(skill).toContain('直接在当前项目根目录创建三个文件');
|
||||
expect(skill).toContain('只继续维护根目录文件');
|
||||
});
|
||||
});
|
||||
@@ -21,9 +21,9 @@ function LocationProbe() {
|
||||
|
||||
describe('plugin navigation cutover', () => {
|
||||
it.each([
|
||||
['/plugin-marketplace', `${PROJECT_SETTINGS_PLUGINS_PATH}?scope=all&source=official`],
|
||||
['/plugin-marketplace', `${PROJECT_SETTINGS_PLUGINS_PATH}?scope=all&source=all&state=available`],
|
||||
['/my-plugins', `${PROJECT_SETTINGS_PLUGINS_PATH}?scope=all&state=mine`],
|
||||
['/project-plugins', `${PROJECT_SETTINGS_PLUGINS_PATH}?scope=project`],
|
||||
['/project-plugins', `${PROJECT_SETTINGS_PLUGINS_PATH}?scope=project&state=mine`],
|
||||
] as const)('replace-redirects %s to %s', async (legacy, canonical) => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/previous', legacy]} initialIndex={1}>
|
||||
|
||||
@@ -129,7 +129,7 @@ function devicePackage(packageId: string, displayName: string, enabled: boolean)
|
||||
resolvedVersion: '1.0.0',
|
||||
source: { kind: 'npm', requested: packageId, resolved: `${packageId}@1.0.0` },
|
||||
kind: 'skill-only',
|
||||
skillEntries: [],
|
||||
skillEntries: [{ id: `${packageId}.skill`, entryPath: 'SKILL.md' }],
|
||||
extensionEntries: [],
|
||||
enabled,
|
||||
confirmedExecutableCode: false,
|
||||
@@ -138,6 +138,45 @@ function devicePackage(packageId: string, displayName: string, enabled: boolean)
|
||||
}
|
||||
|
||||
describe('buildPluginWorkspaceProjection', () => {
|
||||
it('places a user-installed loose Skill only in the installed tab', () => {
|
||||
const looseSkill = {
|
||||
...devicePackage('local-writing', 'Local Writing', true),
|
||||
source: {
|
||||
kind: 'file' as const,
|
||||
requested: '/Users/example/.codex/skills/local-writing/SKILL.md',
|
||||
resolved: '/Users/example/.codex/skills/local-writing/SKILL.md',
|
||||
},
|
||||
};
|
||||
const base = {
|
||||
catalog: null,
|
||||
library: null,
|
||||
marketplaceInstallations: {},
|
||||
project: null,
|
||||
activeProject: null,
|
||||
devicePackages: { schemaVersion: 1 as const, generation: 2, packages: [looseSkill] },
|
||||
};
|
||||
|
||||
const installed = buildPluginWorkspaceProjection(input({
|
||||
...base,
|
||||
filters: { scope: 'all', source: 'all', state: 'mine', search: '' },
|
||||
}));
|
||||
expect(installed.items).toEqual([
|
||||
expect.objectContaining({
|
||||
key: 'local:local-writing',
|
||||
source: 'local',
|
||||
delivery: 'local_installed',
|
||||
local: expect.objectContaining({ kind: 'skill-only' }),
|
||||
}),
|
||||
]);
|
||||
expect(installed.counts).toMatchObject({ available: 0, mine: 1 });
|
||||
|
||||
const available = buildPluginWorkspaceProjection(input({
|
||||
...base,
|
||||
filters: { scope: 'all', source: 'all', state: 'available', search: '' },
|
||||
}));
|
||||
expect(available.items).toEqual([]);
|
||||
});
|
||||
|
||||
it('merges every official source into one item with only its legal commands', () => {
|
||||
const result = buildPluginWorkspaceProjection(input());
|
||||
|
||||
@@ -562,11 +601,11 @@ describe('buildPluginWorkspaceProjection', () => {
|
||||
}));
|
||||
|
||||
const all = projectAll({ scope: 'all', source: 'all', state: 'all', search: '' });
|
||||
expect(all.counts).toEqual({ all: 6, available: 1, mine: 3, enabled: 2, update: 1, unavailable: 2 });
|
||||
expect(all.counts).toEqual({ all: 6, available: 2, mine: 4, enabled: 2, update: 1, unavailable: 2 });
|
||||
|
||||
const expectedByState = {
|
||||
available: ['official:makelore.available'],
|
||||
mine: ['official:makelore.notes', 'local:alpha.package', 'local:zeta.package'],
|
||||
available: ['official:makelore.available', 'official:makelore.suspended'],
|
||||
mine: ['official:makelore.notes', 'local:alpha.package', 'local:zeta.package', 'retained:makelore.ghost'],
|
||||
enabled: ['official:makelore.notes', 'local:alpha.package'],
|
||||
update: ['official:makelore.notes'],
|
||||
unavailable: ['official:makelore.suspended', 'retained:makelore.ghost'],
|
||||
|
||||
@@ -6,19 +6,19 @@ import {
|
||||
} from '@/pages/Plugins/plugin-workspace-query';
|
||||
|
||||
describe('plugin workspace URL state', () => {
|
||||
it('uses the hydrated project presence for a bare route default', () => {
|
||||
it('opens the cloud-backed available tab for a bare route', () => {
|
||||
expect(parsePluginWorkspaceSearch(new URLSearchParams(), true)).toEqual({
|
||||
scope: 'project', source: 'all', state: 'all', search: '', selectedKey: null,
|
||||
scope: 'all', source: 'all', state: 'available', search: '', selectedKey: null,
|
||||
});
|
||||
expect(parsePluginWorkspaceSearch(new URLSearchParams(), false)).toEqual({
|
||||
scope: 'all', source: 'all', state: 'all', search: '', selectedKey: null,
|
||||
scope: 'all', source: 'all', state: 'available', search: '', selectedKey: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('normalizes unknown values and a project scope without a project', () => {
|
||||
const params = new URLSearchParams('scope=unknown&source=remote&state=broken&q=%20notes%20&plugin=notes');
|
||||
expect(parsePluginWorkspaceSearch(params, true)).toEqual({
|
||||
scope: 'project', source: 'all', state: 'all', search: 'notes', selectedKey: null,
|
||||
scope: 'all', source: 'all', state: 'available', search: 'notes', selectedKey: null,
|
||||
});
|
||||
expect(parsePluginWorkspaceSearch(new URLSearchParams('scope=project'), false).scope).toBe('all');
|
||||
});
|
||||
@@ -31,39 +31,44 @@ describe('plugin workspace URL state', () => {
|
||||
|
||||
expect(fallback).toEqual({
|
||||
filters: {
|
||||
scope: 'all', source: 'all', state: 'all', search: '', selectedKey: null,
|
||||
scope: 'all', source: 'all', state: 'available', search: '', selectedKey: null,
|
||||
},
|
||||
canonicalSearch: 'scope=all&source=all&state=all',
|
||||
canonicalSearch: 'scope=all&source=all&state=available',
|
||||
scopeFallback: true,
|
||||
});
|
||||
|
||||
const afterProjectAppears = resolvePluginWorkspaceSearch(new URLSearchParams(), true);
|
||||
expect(afterProjectAppears).toEqual({
|
||||
filters: {
|
||||
scope: 'project', source: 'all', state: 'all', search: '', selectedKey: null,
|
||||
scope: 'all', source: 'all', state: 'available', search: '', selectedKey: null,
|
||||
},
|
||||
canonicalSearch: 'scope=project&source=all&state=all',
|
||||
canonicalSearch: 'scope=all&source=all&state=available',
|
||||
scopeFallback: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('round-trips canonical filters and source-qualified selection', () => {
|
||||
it('round-trips the installed tab and source-qualified selection', () => {
|
||||
const serialized = serializePluginWorkspaceSearch({
|
||||
scope: 'all',
|
||||
source: 'local',
|
||||
state: 'enabled',
|
||||
state: 'mine',
|
||||
search: ' web search ',
|
||||
selectedKey: 'local:makelore_web_search',
|
||||
});
|
||||
expect(serialized.toString()).toBe(
|
||||
'scope=all&source=local&state=enabled&q=web+search&plugin=local%3Amakelore_web_search',
|
||||
'scope=all&source=local&state=mine&q=web+search&plugin=local%3Amakelore_web_search',
|
||||
);
|
||||
expect(parsePluginWorkspaceSearch(serialized, true)).toEqual({
|
||||
scope: 'all',
|
||||
source: 'local',
|
||||
state: 'enabled',
|
||||
state: 'mine',
|
||||
search: 'web search',
|
||||
selectedKey: 'local:makelore_web_search',
|
||||
});
|
||||
});
|
||||
|
||||
it('maps historical hidden filters onto one of the two visible tabs', () => {
|
||||
expect(parsePluginWorkspaceSearch(new URLSearchParams('state=enabled'), true).state).toBe('mine');
|
||||
expect(parsePluginWorkspaceSearch(new URLSearchParams('state=all'), true).state).toBe('available');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -182,6 +182,8 @@ function props(selected: PluginWorkspaceItem | null = null) {
|
||||
sourceLoading: [],
|
||||
dataService: null,
|
||||
dataServicePending: {},
|
||||
activeTab: 'mine' as const,
|
||||
onTabChange: vi.fn(),
|
||||
onSelect: vi.fn(),
|
||||
onCommand: vi.fn(),
|
||||
isCommandPending: vi.fn().mockReturnValue(false),
|
||||
@@ -241,10 +243,15 @@ function preparePluginsRouteState() {
|
||||
|
||||
describe('PluginsView', () => {
|
||||
it('renders a direct Skill-like list in the project drawer without filters or refresh', () => {
|
||||
render(<MemoryRouter><PluginsView {...props()} embedded /></MemoryRouter>);
|
||||
const view = props();
|
||||
render(<MemoryRouter><PluginsView {...view} embedded /></MemoryRouter>);
|
||||
|
||||
expect(screen.getByTestId('plugins-page')).toBeVisible();
|
||||
expect(screen.queryByRole('heading', { name: '插件', level: 1 })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('tab', { name: /可选/ })).toHaveAttribute('aria-selected', 'false');
|
||||
expect(screen.getByRole('tab', { name: /已安装/ })).toHaveAttribute('aria-selected', 'true');
|
||||
fireEvent.mouseDown(screen.getByRole('tab', { name: /可选/ }), { button: 0, ctrlKey: false });
|
||||
expect(view.onTabChange).toHaveBeenCalledWith('available');
|
||||
expect(screen.getByTestId('plugin-list')).toHaveClass('space-y-3');
|
||||
expect(screen.queryByRole('button', { name: /刷新/ })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('searchbox')).not.toBeInTheDocument();
|
||||
@@ -252,6 +259,7 @@ describe('PluginsView', () => {
|
||||
expect(screen.getByRole('button', { name: '查看开发数据服务详情' })).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: '开发数据服务:已添加到项目' })).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: 'Pi Web Search:已安装' })).toBeDisabled();
|
||||
expect(screen.getByText('Skill + 插件')).toBeVisible();
|
||||
});
|
||||
|
||||
it('keeps the production URL and fallback notice aligned without exposing query controls', async () => {
|
||||
@@ -263,7 +271,7 @@ describe('PluginsView', () => {
|
||||
);
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
|
||||
'/project-config/plugins?scope=all&source=all&state=all',
|
||||
'/project-config/plugins?scope=all&source=all&state=available',
|
||||
));
|
||||
expect(screen.queryByLabelText('范围')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('当前没有打开的项目,已显示全部插件。')).toBeVisible();
|
||||
@@ -278,25 +286,25 @@ describe('PluginsView', () => {
|
||||
act(() => codingWorkspaceStore.setState({ activeProjectId: activeProject.id, activeProject }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开裸插件地址' }));
|
||||
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
|
||||
'/project-config/plugins?scope=project&source=all&state=all',
|
||||
'/project-config/plugins?scope=all&source=all&state=available',
|
||||
));
|
||||
expect(screen.queryByLabelText('范围')).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开非法插件地址' }));
|
||||
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
|
||||
'/project-config/plugins?scope=project&source=all&state=all',
|
||||
'/project-config/plugins?scope=all&source=all&state=available',
|
||||
));
|
||||
expect(screen.queryByLabelText('范围')).not.toBeInTheDocument();
|
||||
|
||||
act(() => codingWorkspaceStore.setState({ activeProjectId: null, activeProject: null }));
|
||||
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
|
||||
'/project-config/plugins?scope=all&source=all&state=all',
|
||||
'/project-config/plugins?scope=all&source=all&state=available',
|
||||
));
|
||||
expect(screen.queryByLabelText('范围')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('当前没有打开的项目,已显示全部插件。')).toBeVisible();
|
||||
expect(screen.queryByText('当前没有打开的项目,已显示全部插件。')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps available rows visible when individual sources fail to load', async () => {
|
||||
it('keeps installed rows visible when individual sources fail to load', async () => {
|
||||
const activeProject = {
|
||||
id: 'project-a',
|
||||
name: 'Project A',
|
||||
@@ -344,7 +352,7 @@ describe('PluginsView', () => {
|
||||
vi.spyOn(devicePackageStore.getState(), 'load').mockResolvedValue(undefined);
|
||||
vi.spyOn(codingPluginsStore.getState(), 'load').mockResolvedValue(undefined);
|
||||
|
||||
render(<MemoryRouter initialEntries={['/project-config/plugins?scope=project&source=all&state=all']}><Plugins /></MemoryRouter>);
|
||||
render(<MemoryRouter initialEntries={['/project-config/plugins?scope=project&source=all&state=mine']}><Plugins /></MemoryRouter>);
|
||||
|
||||
for (const name of ['开发数据服务', 'Pi Web Search', 'makelore.retained']) {
|
||||
expect(await screen.findByRole('heading', { name })).toBeVisible();
|
||||
@@ -353,6 +361,73 @@ describe('PluginsView', () => {
|
||||
expect(screen.queryByText('缓存')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('clears legacy source filters when switching to installed so local Skills remain visible', async () => {
|
||||
preparePluginsRouteState();
|
||||
pluginMarketplaceStore.setState({
|
||||
catalog: {
|
||||
items: [{
|
||||
pluginId: 'makelore.notes',
|
||||
title: 'Notes',
|
||||
summary: 'Write project notes',
|
||||
category: 'productivity',
|
||||
tags: ['notes'],
|
||||
providerDisplayName: 'MakeLore',
|
||||
runtimeKind: 'skill_only',
|
||||
runtimeStatus: 'enabled',
|
||||
acquisition: 'free',
|
||||
usageBilling: 'included',
|
||||
includedOperationCount: 1,
|
||||
meteredOperationCount: 0,
|
||||
stableVersion: '1.0.0',
|
||||
betaVersion: null,
|
||||
}],
|
||||
nextCursor: null,
|
||||
total: 1,
|
||||
catalogGeneration: 1,
|
||||
etag: 'catalog-1',
|
||||
pricingVersionId: null,
|
||||
stale: false,
|
||||
fetchedAt: 1,
|
||||
},
|
||||
});
|
||||
devicePackageStore.setState({
|
||||
index: {
|
||||
schemaVersion: 1,
|
||||
generation: 2,
|
||||
packages: [{
|
||||
...localItem.local!,
|
||||
packageId: 'local-writing',
|
||||
displayName: 'Local Writing',
|
||||
source: {
|
||||
kind: 'file',
|
||||
requested: '/Users/example/.codex/skills/local-writing/SKILL.md',
|
||||
resolved: '/Users/example/.codex/skills/local-writing/SKILL.md',
|
||||
},
|
||||
kind: 'skill-only',
|
||||
skillEntries: [{ id: 'local-writing', entryPath: 'SKILL.md' }],
|
||||
extensionEntries: [],
|
||||
confirmedExecutableCode: false,
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={[
|
||||
'/project-config/plugins?scope=all&source=official&state=available',
|
||||
]}>
|
||||
<PluginsRouteHarness />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(await screen.findByRole('heading', { name: 'Notes' })).toBeVisible();
|
||||
fireEvent.mouseDown(screen.getByRole('tab', { name: /已安装/ }), { button: 0, ctrlKey: false });
|
||||
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
|
||||
'/project-config/plugins?scope=all&source=all&state=mine',
|
||||
));
|
||||
expect(await screen.findByRole('heading', { name: 'Local Writing' })).toBeVisible();
|
||||
expect(screen.getByText('Skill')).toBeVisible();
|
||||
});
|
||||
|
||||
it('attempts a rejected official detail load once per open and retries only after close and reopen', async () => {
|
||||
preparePluginsRouteState();
|
||||
pluginMarketplaceStore.setState({
|
||||
@@ -393,7 +468,7 @@ describe('PluginsView', () => {
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={[
|
||||
'/project-config/plugins?scope=all&source=all&state=all&plugin=official%3Amakelore.notes',
|
||||
'/project-config/plugins?scope=all&source=all&state=available&plugin=official%3Amakelore.notes',
|
||||
]}>
|
||||
<Plugins />
|
||||
</MemoryRouter>,
|
||||
@@ -431,7 +506,7 @@ describe('PluginsView', () => {
|
||||
render(<MemoryRouter><PluginsView {...view} /></MemoryRouter>);
|
||||
|
||||
expect(screen.getByRole('heading', { name: '插件' })).toBeVisible();
|
||||
expect(screen.getByText('为你的智能体添加插件,拓展更多能力。')).toBeVisible();
|
||||
expect(screen.getByText('插件与 Skill 统一管理;云端能力在这里下发,本机安装的 Skill 也会集中显示。')).toBeVisible();
|
||||
expect(screen.getByText('开发数据服务')).toBeVisible();
|
||||
expect(screen.getByText('Pi Web Search')).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: '查看开发数据服务详情' })).toBeVisible();
|
||||
@@ -471,7 +546,7 @@ describe('PluginsView', () => {
|
||||
view.projection = {
|
||||
items: [scaffoldItem],
|
||||
selected: scaffoldItem,
|
||||
counts: { all: 1, available: 0, mine: 0, enabled: 0, update: 0, unavailable: 0 },
|
||||
counts: { all: 1, available: 1, mine: 0, enabled: 0, update: 0, unavailable: 0 },
|
||||
notices: [],
|
||||
};
|
||||
|
||||
@@ -531,7 +606,7 @@ describe('PluginsView', () => {
|
||||
render(<MemoryRouter><PluginsView {...props(scriptSkill)} /></MemoryRouter>);
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: 'MakeLore 项目初始化' });
|
||||
expect(dialog).toHaveTextContent('为智能体提供可在本机使用的扩展能力。');
|
||||
expect(dialog).toHaveTextContent('这是用户自行安装在本机的 Skill,已统一收纳在插件模块中。');
|
||||
expect(dialog).not.toHaveTextContent('Skill 脚本');
|
||||
expect(dialog).not.toHaveTextContent('Pi extension');
|
||||
expect(dialog).not.toHaveTextContent('完整桌面权限');
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getSkillDisplayInfo } from '@/lib/skill-display';
|
||||
|
||||
describe('skill display metadata', () => {
|
||||
it('keeps runtime ids internal and returns Chinese-only learner-facing copy', () => {
|
||||
expect(getSkillDisplayInfo('agent-browser')).toEqual({
|
||||
name: '开发浏览器',
|
||||
description: '打开、查看或调试本地及公网网页,读取 Console、Network、DOM 和样式信息。',
|
||||
});
|
||||
expect(getSkillDisplayInfo('data-service')).toEqual({
|
||||
name: '开发数据服务',
|
||||
description: '为本地项目配置受控的开发数据,并在预览中验证真实读写。',
|
||||
});
|
||||
expect(getSkillDisplayInfo('frontend-slides')).toEqual({
|
||||
name: '项目演示',
|
||||
description: '把项目内容整理成可播放的 16:9 HTML 幻灯片,不生成 PPTX 或云端发布。',
|
||||
});
|
||||
expect(getSkillDisplayInfo('grilling')).toEqual({
|
||||
name: '方案质询',
|
||||
description: '在重要工作开始前逐项澄清目标、范围和关键取舍,确认后再执行。',
|
||||
});
|
||||
expect(getSkillDisplayInfo('planning-with-files')).toEqual({
|
||||
name: '项目规划',
|
||||
description: '为复杂任务维护可恢复的计划、发现和进展,直接记录在当前项目根目录。',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not expose an unknown external runtime id', () => {
|
||||
const display = getSkillDisplayInfo('repo-audit');
|
||||
expect(display).toEqual({
|
||||
name: '自定义技能',
|
||||
description: '用于扩展智能体工作能力的自定义技能。',
|
||||
});
|
||||
expect(`${display.name}${display.description}`).not.toContain('repo-audit');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user