fix(extensions): unlock structured skill editing after reset

This commit is contained in:
777genius 2026-04-16 23:06:05 +03:00
parent 3e5ec7c173
commit 4578f11dd7
2 changed files with 246 additions and 0 deletions

View file

@ -730,6 +730,7 @@ export const SkillEditorDialog = ({
size="sm"
onClick={() => {
setManualRawEdit(false);
setCustomMarkdownDetected(false);
const nextRawContent = buildSkillTemplate({
name,
description,

View file

@ -0,0 +1,245 @@
import React, { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { SkillDetail } from '@shared/types/extensions';
interface StoreState {
previewSkillUpsert: ReturnType<typeof vi.fn>;
applySkillUpsert: ReturnType<typeof vi.fn>;
}
const storeState = {} as StoreState;
vi.mock('@renderer/store', () => ({
useStore: (selector: (state: StoreState) => unknown) => selector(storeState),
}));
vi.mock('@renderer/hooks/useMarkdownScrollSync', () => ({
useMarkdownScrollSync: () => ({
handleCodeScroll: vi.fn(),
handlePreviewScroll: vi.fn(),
previewScrollRef: { current: null },
}),
}));
vi.mock('@renderer/components/team/editor/MarkdownPreviewPane', () => ({
MarkdownPreviewPane: () => React.createElement('div', null, 'Preview'),
}));
vi.mock('@renderer/components/ui/badge', () => ({
Badge: ({ children }: React.PropsWithChildren) => React.createElement('span', null, children),
}));
vi.mock('@renderer/components/ui/button', () => ({
Button: ({
children,
onClick,
type = 'button',
disabled,
}: React.PropsWithChildren<{
onClick?: () => void;
type?: 'button' | 'submit' | 'reset';
disabled?: boolean;
}>) =>
React.createElement(
'button',
{
type,
disabled,
onClick,
},
children
),
}));
vi.mock('@renderer/components/ui/checkbox', () => ({
Checkbox: ({
checked,
onCheckedChange,
}: {
checked?: boolean;
onCheckedChange?: (value: boolean) => void;
className?: string;
}) =>
React.createElement('input', {
type: 'checkbox',
checked,
onChange: (event: React.ChangeEvent<HTMLInputElement>) =>
onCheckedChange?.(event.target.checked),
}),
}));
vi.mock('@renderer/components/ui/dialog', () => ({
Dialog: ({ open, children }: React.PropsWithChildren<{ open: boolean }>) =>
open ? React.createElement('div', null, children) : null,
DialogContent: ({ children }: React.PropsWithChildren) => React.createElement('div', null, children),
DialogDescription: ({ children }: React.PropsWithChildren) =>
React.createElement('p', null, children),
DialogHeader: ({ children }: React.PropsWithChildren) => React.createElement('div', null, children),
DialogTitle: ({ children }: React.PropsWithChildren) => React.createElement('h2', null, children),
}));
vi.mock('@renderer/components/ui/input', () => ({
Input: (props: React.InputHTMLAttributes<HTMLInputElement>) =>
React.createElement('input', props),
}));
vi.mock('@renderer/components/ui/label', () => ({
Label: ({ children, htmlFor }: React.PropsWithChildren<{ htmlFor?: string }>) =>
React.createElement('label', { htmlFor }, children),
}));
vi.mock('@renderer/components/ui/select', () => ({
Select: ({
children,
value,
onValueChange,
disabled,
}: React.PropsWithChildren<{
value: string;
onValueChange: (value: string) => void;
disabled?: boolean;
}>) =>
React.createElement(
'select',
{
value,
disabled,
onChange: (event: React.ChangeEvent<HTMLSelectElement>) => onValueChange(event.target.value),
},
children
),
SelectTrigger: () => null,
SelectValue: () => null,
SelectContent: ({ children }: React.PropsWithChildren) =>
React.createElement(React.Fragment, null, children),
SelectItem: ({
children,
value,
disabled,
}: React.PropsWithChildren<{ value: string; disabled?: boolean }>) =>
React.createElement('option', { value, disabled }, children),
}));
vi.mock('@renderer/components/ui/textarea', () => ({
Textarea: (props: React.TextareaHTMLAttributes<HTMLTextAreaElement>) =>
React.createElement('textarea', props),
}));
vi.mock('@renderer/components/extensions/skills/SkillCodeEditor', () => ({
SkillCodeEditor: () => React.createElement('div', null, 'Editor'),
}));
vi.mock('@renderer/components/extensions/skills/SkillReviewDialog', () => ({
SkillReviewDialog: () => null,
}));
vi.mock('lucide-react', () => {
const Icon = (props: React.SVGProps<SVGSVGElement>) => React.createElement('svg', props);
return {
FileSearch: Icon,
RotateCcw: Icon,
X: Icon,
};
});
import { SkillEditorDialog } from '@renderer/components/extensions/skills/SkillEditorDialog';
function makeDetail(rawContent: string): SkillDetail {
return {
item: {
id: '/tmp/project/.claude/skills/custom-skill',
sourceType: 'filesystem',
name: 'Custom Skill',
description: 'Custom markdown skill',
folderName: 'custom-skill',
scope: 'project',
rootKind: 'claude',
projectRoot: '/tmp/project',
discoveryRoot: '/tmp/project/.claude/skills',
skillDir: '/tmp/project/.claude/skills/custom-skill',
skillFile: '/tmp/project/.claude/skills/custom-skill/SKILL.md',
metadata: {},
invocationMode: 'auto',
flags: {
hasScripts: false,
hasReferences: false,
hasAssets: false,
},
isValid: true,
issues: [],
modifiedAt: 1,
},
body: rawContent,
rawContent,
rawFrontmatter: null,
referencesFiles: [],
scriptFiles: [],
assetFiles: [],
};
}
describe('SkillEditorDialog', () => {
beforeEach(() => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
storeState.previewSkillUpsert = vi.fn();
storeState.applySkillUpsert = vi.fn();
});
afterEach(() => {
document.body.innerHTML = '';
vi.unstubAllGlobals();
});
it('unlocks structured editing after resetting a custom markdown skill', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
const detail = makeDetail(`---
name: Custom Skill
description: Custom markdown skill
---
# Custom Skill
This file uses a freeform layout without generated sections.
`);
await act(async () => {
root.render(
React.createElement(SkillEditorDialog, {
open: true,
mode: 'edit',
projectPath: '/tmp/project',
projectLabel: 'Project',
detail,
onClose: vi.fn(),
onSaved: vi.fn(),
})
);
await Promise.resolve();
});
expect(host.querySelector('#skill-when-to-use')).toBeNull();
const resetButton = Array.from(host.querySelectorAll('button')).find((button) =>
button.textContent?.includes('Reset From Structured Fields')
) as HTMLButtonElement;
expect(resetButton).toBeDefined();
await act(async () => {
resetButton.click();
await Promise.resolve();
});
const whenToUseField = host.querySelector('#skill-when-to-use') as HTMLTextAreaElement;
expect(whenToUseField).not.toBeNull();
expect(whenToUseField.disabled).toBe(false);
await act(async () => {
root.unmount();
await Promise.resolve();
});
});
});