mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
Clicking Edit on a built-in skill silently issued PUT /api/skills/:id, which the daemon stores as a user-owned shadow and then hides the built-in entry from the Settings list. From the user's perspective the row they just edited disappears with no explanation. Arm an inline override-warning banner on Edit for source==='built-in' skills, mirroring the existing inline delete-confirm pattern. The edit form only opens after the user explicitly confirms. User skills bypass the warning and edit directly. No daemon or listSkills contract change. Fixes #1378
158 lines
4.7 KiB
TypeScript
158 lines
4.7 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { SkillsSection } from '../../src/components/SkillsSection';
|
|
import type { AppConfig } from '../../src/types';
|
|
import type { SkillSummary } from '@open-design/contracts';
|
|
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
const TEST_CONFIG: AppConfig = {
|
|
mode: 'daemon',
|
|
apiKey: '',
|
|
baseUrl: '',
|
|
model: '',
|
|
agentId: null,
|
|
skillId: null,
|
|
designSystemId: null,
|
|
disabledSkills: [],
|
|
};
|
|
|
|
function makeSkill(overrides: Partial<SkillSummary>): SkillSummary {
|
|
return {
|
|
id: 'skill',
|
|
name: 'Skill',
|
|
description: 'A skill',
|
|
triggers: [],
|
|
mode: 'prototype',
|
|
previewType: 'html',
|
|
designSystemRequired: true,
|
|
defaultFor: [],
|
|
upstream: null,
|
|
hasBody: true,
|
|
examplePrompt: '',
|
|
aggregatesExamples: false,
|
|
source: 'built-in',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function renderSkillsSection(skills: SkillSummary[]) {
|
|
const setCfg = vi.fn();
|
|
globalThis.fetch = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
const url = input.toString();
|
|
if (url === '/api/skills' && (!init || init.method === undefined)) {
|
|
return new Response(JSON.stringify({ skills }), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
}
|
|
if (url.startsWith('/api/skills/') && init?.method === 'DELETE') {
|
|
return new Response(JSON.stringify({ ok: true }), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
}
|
|
return new Response(JSON.stringify({}), { status: 404 });
|
|
}) as typeof fetch;
|
|
|
|
render(
|
|
<SkillsSection
|
|
cfg={TEST_CONFIG}
|
|
setCfg={setCfg}
|
|
/>,
|
|
);
|
|
return { fetchMock: globalThis.fetch as ReturnType<typeof vi.fn>, setCfg };
|
|
}
|
|
|
|
describe('SkillsSection', () => {
|
|
afterEach(() => {
|
|
cleanup();
|
|
globalThis.fetch = originalFetch;
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('does not expose delete actions for built-in skills', async () => {
|
|
renderSkillsSection([
|
|
makeSkill({
|
|
id: 'builtin-skill',
|
|
name: 'Built-in skill',
|
|
source: 'built-in',
|
|
}),
|
|
]);
|
|
|
|
const row = await screen.findByTestId('skill-row-builtin-skill');
|
|
|
|
expect(within(row).queryByTestId('skills-delete')).toBeNull();
|
|
expect(within(row).queryByTestId('skills-delete-confirm')).toBeNull();
|
|
});
|
|
|
|
it('keeps delete confirmation and commit available for user skills', async () => {
|
|
const { fetchMock } = renderSkillsSection([
|
|
makeSkill({
|
|
id: 'user-skill',
|
|
name: 'User skill',
|
|
source: 'user',
|
|
}),
|
|
]);
|
|
|
|
const row = await screen.findByTestId('skill-row-user-skill');
|
|
fireEvent.click(within(row).getByTestId('skills-delete'));
|
|
fireEvent.click(within(row).getByTestId('skills-delete-confirm'));
|
|
|
|
await waitFor(() => {
|
|
expect(fetchMock).toHaveBeenCalledWith('/api/skills/user-skill', {
|
|
method: 'DELETE',
|
|
});
|
|
});
|
|
});
|
|
|
|
it('warns before editing a built-in skill creates a user override', async () => {
|
|
const { fetchMock } = renderSkillsSection([
|
|
makeSkill({
|
|
id: 'builtin-skill',
|
|
name: 'Built-in skill',
|
|
source: 'built-in',
|
|
}),
|
|
]);
|
|
|
|
const row = await screen.findByTestId('skill-row-builtin-skill');
|
|
fireEvent.click(within(row).getByTestId('skills-edit'));
|
|
|
|
const warning = await within(row).findByTestId('skills-edit-builtin-warning');
|
|
expect(warning.textContent).toMatch(/override/i);
|
|
expect(within(row).queryByTestId('skills-edit-form')).toBeNull();
|
|
expect(fetchMock).not.toHaveBeenCalledWith(
|
|
'/api/skills/builtin-skill',
|
|
expect.objectContaining({ method: 'PUT' }),
|
|
);
|
|
|
|
fireEvent.click(within(row).getByTestId('skills-edit-builtin-cancel'));
|
|
expect(within(row).queryByTestId('skills-edit-builtin-warning')).toBeNull();
|
|
expect(within(row).queryByTestId('skills-edit-form')).toBeNull();
|
|
|
|
fireEvent.click(within(row).getByTestId('skills-edit'));
|
|
fireEvent.click(
|
|
await within(row).findByTestId('skills-edit-builtin-confirm'),
|
|
);
|
|
expect(await within(row).findByTestId('skills-edit-form')).toBeTruthy();
|
|
});
|
|
|
|
it('skips the override warning when editing a user skill', async () => {
|
|
renderSkillsSection([
|
|
makeSkill({
|
|
id: 'user-skill',
|
|
name: 'User skill',
|
|
source: 'user',
|
|
}),
|
|
]);
|
|
|
|
const row = await screen.findByTestId('skill-row-user-skill');
|
|
fireEvent.click(within(row).getByTestId('skills-edit'));
|
|
|
|
expect(within(row).queryByTestId('skills-edit-builtin-warning')).toBeNull();
|
|
expect(await within(row).findByTestId('skills-edit-form')).toBeTruthy();
|
|
});
|
|
});
|