fix(extensions): reset stale skill review state on close

This commit is contained in:
777genius 2026-04-16 23:11:13 +03:00
parent 2142518461
commit 2201b54f28
4 changed files with 173 additions and 2 deletions

View file

@ -219,6 +219,18 @@ export const SkillEditorDialog = ({
setMutationError(null);
}, [detail, mode, open, projectPath]);
useEffect(() => {
if (open) {
return;
}
setReviewPreview(null);
setReviewOpen(false);
setReviewLoading(false);
setSaveLoading(false);
setMutationError(null);
}, [open]);
useEffect(() => {
if (open && mode === 'create' && scope === 'project' && !projectPath) {
setScope('user');

View file

@ -93,6 +93,18 @@ export const SkillImportDialog = ({
setMutationError(null);
}, [open, projectPath]);
useEffect(() => {
if (open) {
return;
}
setPreview(null);
setReviewOpen(false);
setReviewLoading(false);
setImportLoading(false);
setMutationError(null);
}, [open]);
useEffect(() => {
if (!open || folderNameEdited) {
return;

View file

@ -132,7 +132,8 @@ vi.mock('@renderer/components/extensions/skills/SkillCodeEditor', () => ({
}));
vi.mock('@renderer/components/extensions/skills/SkillReviewDialog', () => ({
SkillReviewDialog: () => null,
SkillReviewDialog: ({ open }: { open: boolean }) =>
open ? React.createElement('div', { 'data-testid': 'skill-review-dialog' }, 'Review') : null,
}));
vi.mock('lucide-react', () => {
@ -242,4 +243,74 @@ This file uses a freeform layout without generated sections.
await Promise.resolve();
});
});
it('clears review state when the editor closes externally', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
storeState.previewSkillUpsert.mockResolvedValue({
planId: 'plan-1',
targetSkillDir: '/tmp/project/.claude/skills/new-skill',
changes: [
{
relativePath: 'SKILL.md',
absolutePath: '/tmp/project/.claude/skills/new-skill/SKILL.md',
action: 'create',
oldContent: null,
newContent: '# Skill',
isBinary: false,
},
],
warnings: [],
summary: { created: 1, updated: 0, deleted: 0, binary: 0 },
});
await act(async () => {
root.render(
React.createElement(SkillEditorDialog, {
open: true,
mode: 'create',
projectPath: '/tmp/project',
projectLabel: 'Project',
detail: null,
onClose: vi.fn(),
onSaved: vi.fn(),
})
);
await Promise.resolve();
});
const reviewButton = Array.from(host.querySelectorAll('button')).find((button) =>
button.textContent?.includes('Review And Create')
) as HTMLButtonElement;
await act(async () => {
reviewButton.click();
await Promise.resolve();
await Promise.resolve();
});
expect(host.querySelector('[data-testid="skill-review-dialog"]')).not.toBeNull();
await act(async () => {
root.render(
React.createElement(SkillEditorDialog, {
open: false,
mode: 'create',
projectPath: '/tmp/project',
projectLabel: 'Project',
detail: null,
onClose: vi.fn(),
onSaved: vi.fn(),
})
);
await Promise.resolve();
});
expect(host.querySelector('[data-testid="skill-review-dialog"]')).toBeNull();
await act(async () => {
root.unmount();
await Promise.resolve();
});
});
});

View file

@ -93,7 +93,8 @@ vi.mock('@renderer/components/ui/select', () => ({
}));
vi.mock('@renderer/components/extensions/skills/SkillReviewDialog', () => ({
SkillReviewDialog: () => null,
SkillReviewDialog: ({ open }: { open: boolean }) =>
open ? React.createElement('div', { 'data-testid': 'skill-review-dialog' }, 'Review') : null,
}));
vi.mock('lucide-react', () => {
@ -302,4 +303,79 @@ describe('SkillImportDialog', () => {
await Promise.resolve();
});
});
it('clears review state when the import dialog closes externally', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
storeState.previewSkillImport.mockResolvedValue({
planId: 'plan-1',
targetSkillDir: '/tmp/imported-skill',
changes: [
{
relativePath: 'SKILL.md',
absolutePath: '/tmp/imported-skill/SKILL.md',
action: 'create',
oldContent: null,
newContent: '# Skill',
isBinary: false,
},
],
warnings: [],
summary: { created: 1, updated: 0, deleted: 0, binary: 0 },
});
await act(async () => {
root.render(
React.createElement(SkillImportDialog, {
open: true,
projectPath: null,
projectLabel: null,
onClose: vi.fn(),
onImported: vi.fn(),
})
);
await Promise.resolve();
});
const sourceInput = host.querySelector('#skill-import-source') as HTMLInputElement;
await act(async () => {
const setValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
setValue?.call(sourceInput, '/tmp/source-skill');
sourceInput.dispatchEvent(new Event('input', { bubbles: true }));
await Promise.resolve();
});
const reviewButton = Array.from(host.querySelectorAll('button')).find((button) =>
button.textContent?.includes('Review And Import')
) as HTMLButtonElement;
await act(async () => {
reviewButton.click();
await Promise.resolve();
await Promise.resolve();
});
expect(host.querySelector('[data-testid="skill-review-dialog"]')).not.toBeNull();
await act(async () => {
root.render(
React.createElement(SkillImportDialog, {
open: false,
projectPath: null,
projectLabel: null,
onClose: vi.fn(),
onImported: vi.fn(),
})
);
await Promise.resolve();
});
expect(host.querySelector('[data-testid="skill-review-dialog"]')).toBeNull();
await act(async () => {
root.unmount();
await Promise.resolve();
});
});
});