fix(extensions): validate skill import source paths locally
This commit is contained in:
parent
2cc7dc5178
commit
1de59cb84f
4 changed files with 83 additions and 8 deletions
|
|
@ -25,7 +25,7 @@ import { FileSearch, FolderOpen, X } from 'lucide-react';
|
||||||
import { getSuggestedSkillFolderNameFromPath } from './skillFolderNameUtils';
|
import { getSuggestedSkillFolderNameFromPath } from './skillFolderNameUtils';
|
||||||
import { SkillReviewDialog } from './SkillReviewDialog';
|
import { SkillReviewDialog } from './SkillReviewDialog';
|
||||||
import { resolveSkillProjectPath } from './skillProjectUtils';
|
import { resolveSkillProjectPath } from './skillProjectUtils';
|
||||||
import { validateSkillFolderName } from './skillValidationUtils';
|
import { validateSkillFolderName, validateSkillImportSourceDir } from './skillValidationUtils';
|
||||||
|
|
||||||
import type { SkillReviewPreview } from '@shared/types/extensions';
|
import type { SkillReviewPreview } from '@shared/types/extensions';
|
||||||
|
|
||||||
|
|
@ -127,8 +127,16 @@ export const SkillImportDialog = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleReview(): Promise<void> {
|
async function handleReview(): Promise<void> {
|
||||||
|
const normalizedSourceDir = sourceDir.trim();
|
||||||
|
const normalizedFolderName = folderName.trim();
|
||||||
|
const sourceDirError = validateSkillImportSourceDir(sourceDir);
|
||||||
|
if (sourceDirError) {
|
||||||
|
setMutationError(sourceDirError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const folderNameError =
|
const folderNameError =
|
||||||
folderName.trim().length > 0 ? validateSkillFolderName(folderName) : null;
|
normalizedFolderName.length > 0 ? validateSkillFolderName(normalizedFolderName) : null;
|
||||||
if (folderNameError) {
|
if (folderNameError) {
|
||||||
setMutationError(folderNameError);
|
setMutationError(folderNameError);
|
||||||
return;
|
return;
|
||||||
|
|
@ -138,8 +146,8 @@ export const SkillImportDialog = ({
|
||||||
setMutationError(null);
|
setMutationError(null);
|
||||||
try {
|
try {
|
||||||
const nextPreview = await previewSkillImport({
|
const nextPreview = await previewSkillImport({
|
||||||
sourceDir,
|
sourceDir: normalizedSourceDir,
|
||||||
folderName: folderName || undefined,
|
folderName: normalizedFolderName || undefined,
|
||||||
scope,
|
scope,
|
||||||
rootKind,
|
rootKind,
|
||||||
projectPath: resolveSkillProjectPath(scope, projectPath),
|
projectPath: resolveSkillProjectPath(scope, projectPath),
|
||||||
|
|
@ -158,12 +166,15 @@ export const SkillImportDialog = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleConfirmImport(): Promise<void> {
|
async function handleConfirmImport(): Promise<void> {
|
||||||
|
const normalizedSourceDir = sourceDir.trim();
|
||||||
|
const normalizedFolderName = folderName.trim();
|
||||||
|
|
||||||
setImportLoading(true);
|
setImportLoading(true);
|
||||||
setMutationError(null);
|
setMutationError(null);
|
||||||
try {
|
try {
|
||||||
const detail = await applySkillImport({
|
const detail = await applySkillImport({
|
||||||
sourceDir,
|
sourceDir: normalizedSourceDir,
|
||||||
folderName: folderName || undefined,
|
folderName: normalizedFolderName || undefined,
|
||||||
scope,
|
scope,
|
||||||
rootKind,
|
rootKind,
|
||||||
projectPath: resolveSkillProjectPath(scope, projectPath),
|
projectPath: resolveSkillProjectPath(scope, projectPath),
|
||||||
|
|
@ -296,7 +307,7 @@ export const SkillImportDialog = ({
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => void handleReview()}
|
onClick={() => void handleReview()}
|
||||||
disabled={!sourceDir || reviewLoading || importLoading}
|
disabled={!sourceDir.trim() || reviewLoading || importLoading}
|
||||||
>
|
>
|
||||||
<FileSearch className="mr-1.5 size-3.5" />
|
<FileSearch className="mr-1.5 size-3.5" />
|
||||||
{reviewLoading ? 'Preparing...' : 'Review And Import'}
|
{reviewLoading ? 'Preparing...' : 'Review And Import'}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
const MAX_SKILL_FOLDER_NAME_LENGTH = 255;
|
const MAX_SKILL_FOLDER_NAME_LENGTH = 255;
|
||||||
const INVALID_SKILL_FOLDER_NAME_CHARS = /[\x00-\x1f/\\:*?"<>|]/u;
|
const INVALID_SKILL_FOLDER_NAME_CHARS = /[\x00-\x1f/\\:*?"<>|]/u;
|
||||||
|
|
||||||
|
export function validateSkillImportSourceDir(value: string): string | null {
|
||||||
|
if (value.trim().length === 0) {
|
||||||
|
return 'Choose a skill folder to import.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function validateSkillFolderName(value: string): string | null {
|
export function validateSkillFolderName(value: string): string | null {
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
if (trimmed.length === 0) {
|
if (trimmed.length === 0) {
|
||||||
|
|
|
||||||
|
|
@ -426,4 +426,49 @@ describe('SkillImportDialog', () => {
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps review disabled for whitespace-only source folders', async () => {
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
|
||||||
|
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, ' ');
|
||||||
|
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;
|
||||||
|
|
||||||
|
expect(reviewButton.disabled).toBe(true);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
reviewButton.click();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(storeState.previewSkillImport).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,19 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { validateSkillFolderName } from '../../../../../src/renderer/components/extensions/skills/skillValidationUtils';
|
import {
|
||||||
|
validateSkillFolderName,
|
||||||
|
validateSkillImportSourceDir,
|
||||||
|
} from '../../../../../src/renderer/components/extensions/skills/skillValidationUtils';
|
||||||
|
|
||||||
describe('skillValidationUtils', () => {
|
describe('skillValidationUtils', () => {
|
||||||
|
it('rejects empty import source folders', () => {
|
||||||
|
expect(validateSkillImportSourceDir(' ')).toBe('Choose a skill folder to import.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts non-empty import source folders', () => {
|
||||||
|
expect(validateSkillImportSourceDir('/tmp/source-skill')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it('accepts normal folder names', () => {
|
it('accepts normal folder names', () => {
|
||||||
expect(validateSkillFolderName('review-helper')).toBeNull();
|
expect(validateSkillFolderName('review-helper')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue