fix(extensions): gate codex skill overlays by runtime
This commit is contained in:
parent
8423656b97
commit
14a38212c2
7 changed files with 264 additions and 11 deletions
|
|
@ -52,6 +52,7 @@ interface SkillEditorDialogProps {
|
|||
mode: EditorMode;
|
||||
projectPath: string | null;
|
||||
projectLabel: string | null;
|
||||
allowCodexRootKind: boolean;
|
||||
detail: SkillDetail | null;
|
||||
onClose: () => void;
|
||||
onSaved: (skillId: string | null) => void;
|
||||
|
|
@ -70,6 +71,7 @@ export const SkillEditorDialog = ({
|
|||
mode,
|
||||
projectPath,
|
||||
projectLabel,
|
||||
allowCodexRootKind,
|
||||
detail,
|
||||
onClose,
|
||||
onSaved,
|
||||
|
|
@ -220,7 +222,7 @@ export const SkillEditorDialog = ({
|
|||
setReviewLoading(false);
|
||||
setSaveLoading(false);
|
||||
setMutationError(null);
|
||||
}, [detail, mode, open, projectPath]);
|
||||
}, [allowCodexRootKind, detail, mode, open, projectPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
|
|
@ -240,6 +242,12 @@ export const SkillEditorDialog = ({
|
|||
}
|
||||
}, [mode, open, projectPath, scope]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && mode === 'create' && rootKind === 'codex' && !allowCodexRootKind) {
|
||||
setRootKind('claude');
|
||||
}
|
||||
}, [allowCodexRootKind, mode, open, rootKind]);
|
||||
|
||||
useEffect(() => {
|
||||
rawContentRef.current = rawContent;
|
||||
}, [rawContent]);
|
||||
|
|
@ -291,6 +299,14 @@ export const SkillEditorDialog = ({
|
|||
);
|
||||
|
||||
const canUseProjectScope = Boolean(projectPath);
|
||||
const visibleRootDefinitions = useMemo(
|
||||
() =>
|
||||
SKILL_ROOT_DEFINITIONS.filter(
|
||||
(definition) =>
|
||||
definition.rootKind !== 'codex' || allowCodexRootKind || detail?.item.rootKind === 'codex'
|
||||
),
|
||||
[allowCodexRootKind, detail?.item.rootKind]
|
||||
);
|
||||
const instructionsLocked = manualRawEdit || customMarkdownDetected;
|
||||
const title = mode === 'create' ? 'Create skill' : 'Edit skill';
|
||||
const descriptionText =
|
||||
|
|
@ -436,7 +452,7 @@ export const SkillEditorDialog = ({
|
|||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SKILL_ROOT_DEFINITIONS.map((definition) => (
|
||||
{visibleRootDefinitions.map((definition) => (
|
||||
<SelectItem key={definition.rootKind} value={definition.rootKind}>
|
||||
{definition.directoryName}
|
||||
{definition.audience === 'codex' ? ' - Codex only' : ' - Shared'}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ interface SkillImportDialogProps {
|
|||
open: boolean;
|
||||
projectPath: string | null;
|
||||
projectLabel: string | null;
|
||||
allowCodexRootKind: boolean;
|
||||
onClose: () => void;
|
||||
onImported: (skillId: string | null) => void;
|
||||
}
|
||||
|
|
@ -64,6 +65,7 @@ export const SkillImportDialog = ({
|
|||
open,
|
||||
projectPath,
|
||||
projectLabel,
|
||||
allowCodexRootKind,
|
||||
onClose,
|
||||
onImported,
|
||||
}: SkillImportDialogProps): React.JSX.Element => {
|
||||
|
|
@ -120,6 +122,16 @@ export const SkillImportDialog = ({
|
|||
}
|
||||
}, [open, projectPath, scope]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && rootKind === 'codex' && !allowCodexRootKind) {
|
||||
setRootKind('claude');
|
||||
}
|
||||
}, [allowCodexRootKind, open, rootKind]);
|
||||
|
||||
const visibleRootDefinitions = SKILL_ROOT_DEFINITIONS.filter(
|
||||
(definition) => definition.rootKind !== 'codex' || allowCodexRootKind
|
||||
);
|
||||
|
||||
async function handleChooseFolder(): Promise<void> {
|
||||
const selected = await api.config.selectFolders();
|
||||
const first = selected[0];
|
||||
|
|
@ -280,7 +292,7 @@ export const SkillImportDialog = ({
|
|||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SKILL_ROOT_DEFINITIONS.map((definition) => (
|
||||
{visibleRootDefinitions.map((definition) => (
|
||||
<SelectItem key={definition.rootKind} value={definition.rootKind}>
|
||||
{definition.directoryName}
|
||||
{definition.audience === 'codex' ? ' - Codex only' : ' - Shared'}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
formatSkillRootKind,
|
||||
getSkillAudience,
|
||||
getSkillAudienceLabel,
|
||||
isCodexSkillOverlayAvailable,
|
||||
} from '@shared/utils/skillRoots';
|
||||
import {
|
||||
AlertTriangle,
|
||||
|
|
@ -126,11 +127,16 @@ export const SkillsPanel = ({
|
|||
() => [...projectSkills, ...userSkills],
|
||||
[projectSkills, userSkills]
|
||||
);
|
||||
const codexSkillOverlayAvailable = useMemo(
|
||||
() => isCodexSkillOverlayAvailable(cliStatus),
|
||||
[cliStatus]
|
||||
);
|
||||
const codexOnlySkillsCount = useMemo(
|
||||
() => mergedSkills.filter((skill) => getSkillAudience(skill.rootKind) === 'codex').length,
|
||||
[mergedSkills]
|
||||
);
|
||||
const sharedSkillsCount = mergedSkills.length - codexOnlySkillsCount;
|
||||
const showCodexOnlyUi = codexSkillOverlayAvailable || codexOnlySkillsCount > 0;
|
||||
const selectedDetail = selectedSkillId ? (detailById[selectedSkillId] ?? null) : null;
|
||||
selectedSkillItemRef.current = selectedSkillId
|
||||
? (selectedDetail?.item ?? mergedSkills.find((skill) => skill.id === selectedSkillId) ?? null)
|
||||
|
|
@ -266,8 +272,10 @@ export const SkillsPanel = ({
|
|||
</p>
|
||||
<p className="max-w-2xl text-xs leading-5 text-text-muted">
|
||||
Use personal skills for habits you want everywhere. Use project skills for workflows
|
||||
that only make sense inside one codebase. Use `.codex` when a skill should stay
|
||||
Codex-only.
|
||||
that only make sense inside one codebase.
|
||||
{codexSkillOverlayAvailable
|
||||
? ' Use `.codex` when a skill should stay Codex-only.'
|
||||
: ' Existing `.codex` skills stay editable here, but new Codex-only skills need the Codex runtime enabled.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -348,9 +356,11 @@ export const SkillsPanel = ({
|
|||
<Badge variant="secondary" className="font-normal">
|
||||
{sharedSkillsCount} shared
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="font-normal">
|
||||
{codexOnlySkillsCount} Codex only
|
||||
</Badge>
|
||||
{showCodexOnlyUi && (
|
||||
<Badge variant="secondary" className="font-normal">
|
||||
{codexOnlySkillsCount} Codex only
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -363,7 +373,9 @@ export const SkillsPanel = ({
|
|||
['project', 'Project'],
|
||||
['personal', 'Personal'],
|
||||
['shared', 'Shared'],
|
||||
['codex-only', 'Codex only'],
|
||||
...(showCodexOnlyUi
|
||||
? ([['codex-only', 'Codex only']] as [SkillsQuickFilter, string][])
|
||||
: []),
|
||||
['needs-attention', 'Needs attention'],
|
||||
['has-scripts', 'Has scripts'],
|
||||
] as [SkillsQuickFilter, string][]
|
||||
|
|
@ -616,6 +628,7 @@ export const SkillsPanel = ({
|
|||
mode="create"
|
||||
projectPath={projectPath}
|
||||
projectLabel={projectLabel}
|
||||
allowCodexRootKind={codexSkillOverlayAvailable}
|
||||
detail={null}
|
||||
onClose={() => setCreateOpen(false)}
|
||||
onSaved={(skillId) => {
|
||||
|
|
@ -631,6 +644,7 @@ export const SkillsPanel = ({
|
|||
mode="edit"
|
||||
projectPath={projectPath}
|
||||
projectLabel={projectLabel}
|
||||
allowCodexRootKind={codexSkillOverlayAvailable}
|
||||
detail={editingDetail}
|
||||
onClose={() => {
|
||||
setEditOpen(false);
|
||||
|
|
@ -648,6 +662,7 @@ export const SkillsPanel = ({
|
|||
open={importOpen}
|
||||
projectPath={projectPath}
|
||||
projectLabel={projectLabel}
|
||||
allowCodexRootKind={codexSkillOverlayAvailable}
|
||||
onClose={() => setImportOpen(false)}
|
||||
onImported={(skillId) => {
|
||||
setImportOpen(false);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
import {
|
||||
getCliProviderExtensionCapability,
|
||||
isCliExtensionCapabilityAvailable,
|
||||
} from './providerExtensionCapabilities';
|
||||
|
||||
import type { TeamProviderId } from '@shared/types';
|
||||
import type { CliInstallationStatus } from '@shared/types';
|
||||
import type { SkillRootKind } from '@shared/types/extensions';
|
||||
|
||||
export type SkillAudience = 'shared' | 'codex';
|
||||
|
|
@ -59,3 +65,20 @@ export function isSkillAvailableForProvider(
|
|||
): boolean {
|
||||
return getSkillAudience(rootKind) === 'shared' || providerId === 'codex';
|
||||
}
|
||||
|
||||
export function isCodexSkillOverlayAvailable(
|
||||
cliStatus: Pick<CliInstallationStatus, 'flavor' | 'providers'> | null | undefined
|
||||
): boolean {
|
||||
if (cliStatus?.flavor !== 'agent_teams_orchestrator') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const codexProvider = cliStatus.providers.find((provider) => provider.providerId === 'codex');
|
||||
if (!codexProvider?.supported) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isCliExtensionCapabilityAvailable(
|
||||
getCliProviderExtensionCapability(codexProvider, 'skills')
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,6 +181,20 @@ function makeDetail(rawContent: string): SkillDetail {
|
|||
};
|
||||
}
|
||||
|
||||
function makeCodexDetail(rawContent: string): SkillDetail {
|
||||
const detail = makeDetail(rawContent);
|
||||
return {
|
||||
...detail,
|
||||
item: {
|
||||
...detail.item,
|
||||
rootKind: 'codex',
|
||||
discoveryRoot: '/tmp/project/.codex/skills',
|
||||
skillDir: '/tmp/project/.codex/skills/custom-skill',
|
||||
skillFile: '/tmp/project/.codex/skills/custom-skill/SKILL.md',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('SkillEditorDialog', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
|
|
@ -214,6 +228,7 @@ This file uses a freeform layout without generated sections.
|
|||
mode: 'edit',
|
||||
projectPath: '/tmp/project',
|
||||
projectLabel: 'Project',
|
||||
allowCodexRootKind: true,
|
||||
detail,
|
||||
onClose: vi.fn(),
|
||||
onSaved: vi.fn(),
|
||||
|
|
@ -272,6 +287,7 @@ This file uses a freeform layout without generated sections.
|
|||
mode: 'create',
|
||||
projectPath: '/tmp/project',
|
||||
projectLabel: 'Project',
|
||||
allowCodexRootKind: true,
|
||||
detail: null,
|
||||
onClose: vi.fn(),
|
||||
onSaved: vi.fn(),
|
||||
|
|
@ -298,6 +314,7 @@ This file uses a freeform layout without generated sections.
|
|||
mode: 'create',
|
||||
projectPath: '/tmp/project',
|
||||
projectLabel: 'Project',
|
||||
allowCodexRootKind: true,
|
||||
detail: null,
|
||||
onClose: vi.fn(),
|
||||
onSaved: vi.fn(),
|
||||
|
|
@ -326,6 +343,7 @@ This file uses a freeform layout without generated sections.
|
|||
mode: 'create',
|
||||
projectPath: '/tmp/project',
|
||||
projectLabel: 'Project',
|
||||
allowCodexRootKind: true,
|
||||
detail: null,
|
||||
onClose: vi.fn(),
|
||||
onSaved: vi.fn(),
|
||||
|
|
@ -360,4 +378,74 @@ This file uses a freeform layout without generated sections.
|
|||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it('hides the codex root option in create mode when codex runtime is unavailable', async () => {
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
React.createElement(SkillEditorDialog, {
|
||||
open: true,
|
||||
mode: 'create',
|
||||
projectPath: '/tmp/project',
|
||||
projectLabel: 'Project',
|
||||
allowCodexRootKind: false,
|
||||
detail: null,
|
||||
onClose: vi.fn(),
|
||||
onSaved: vi.fn(),
|
||||
})
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const selects = host.querySelectorAll('select');
|
||||
const rootSelect = selects[1] as HTMLSelectElement;
|
||||
expect(Array.from(rootSelect.options).some((option) => option.value === 'codex')).toBe(false);
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the codex root visible when editing an existing codex-only skill', async () => {
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
const detail = makeCodexDetail(`---
|
||||
name: Codex Skill
|
||||
description: Codex markdown skill
|
||||
---
|
||||
|
||||
# Codex Skill
|
||||
`);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
React.createElement(SkillEditorDialog, {
|
||||
open: true,
|
||||
mode: 'edit',
|
||||
projectPath: '/tmp/project',
|
||||
projectLabel: 'Project',
|
||||
allowCodexRootKind: false,
|
||||
detail,
|
||||
onClose: vi.fn(),
|
||||
onSaved: vi.fn(),
|
||||
})
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const selects = host.querySelectorAll('select');
|
||||
const rootSelect = selects[1] as HTMLSelectElement;
|
||||
expect(rootSelect.value).toBe('codex');
|
||||
expect(Array.from(rootSelect.options).some((option) => option.value === 'codex')).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ describe('SkillImportDialog', () => {
|
|||
open: true,
|
||||
projectPath: null,
|
||||
projectLabel: null,
|
||||
allowCodexRootKind: true,
|
||||
onClose: vi.fn(),
|
||||
onImported: vi.fn(),
|
||||
})
|
||||
|
|
@ -167,6 +168,7 @@ describe('SkillImportDialog', () => {
|
|||
open: true,
|
||||
projectPath: null,
|
||||
projectLabel: null,
|
||||
allowCodexRootKind: true,
|
||||
onClose: vi.fn(),
|
||||
onImported: vi.fn(),
|
||||
})
|
||||
|
|
@ -232,6 +234,7 @@ describe('SkillImportDialog', () => {
|
|||
open: true,
|
||||
projectPath: null,
|
||||
projectLabel: null,
|
||||
allowCodexRootKind: true,
|
||||
onClose: vi.fn(),
|
||||
onImported: vi.fn(),
|
||||
})
|
||||
|
|
@ -268,6 +271,7 @@ describe('SkillImportDialog', () => {
|
|||
open: true,
|
||||
projectPath: '/tmp/project-a',
|
||||
projectLabel: 'Project A',
|
||||
allowCodexRootKind: true,
|
||||
onClose: vi.fn(),
|
||||
onImported: vi.fn(),
|
||||
})
|
||||
|
|
@ -284,6 +288,7 @@ describe('SkillImportDialog', () => {
|
|||
open: true,
|
||||
projectPath: null,
|
||||
projectLabel: null,
|
||||
allowCodexRootKind: true,
|
||||
onClose: vi.fn(),
|
||||
onImported: vi.fn(),
|
||||
})
|
||||
|
|
@ -332,6 +337,7 @@ describe('SkillImportDialog', () => {
|
|||
open: true,
|
||||
projectPath: null,
|
||||
projectLabel: null,
|
||||
allowCodexRootKind: true,
|
||||
onClose: vi.fn(),
|
||||
onImported: vi.fn(),
|
||||
})
|
||||
|
|
@ -364,6 +370,7 @@ describe('SkillImportDialog', () => {
|
|||
open: false,
|
||||
projectPath: null,
|
||||
projectLabel: null,
|
||||
allowCodexRootKind: true,
|
||||
onClose: vi.fn(),
|
||||
onImported: vi.fn(),
|
||||
})
|
||||
|
|
@ -390,6 +397,7 @@ describe('SkillImportDialog', () => {
|
|||
open: true,
|
||||
projectPath: null,
|
||||
projectLabel: null,
|
||||
allowCodexRootKind: true,
|
||||
onClose: vi.fn(),
|
||||
onImported: vi.fn(),
|
||||
})
|
||||
|
|
@ -438,6 +446,7 @@ describe('SkillImportDialog', () => {
|
|||
open: true,
|
||||
projectPath: null,
|
||||
projectLabel: null,
|
||||
allowCodexRootKind: true,
|
||||
onClose: vi.fn(),
|
||||
onImported: vi.fn(),
|
||||
})
|
||||
|
|
@ -471,4 +480,33 @@ describe('SkillImportDialog', () => {
|
|||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it('hides the codex root option when codex runtime is unavailable', 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,
|
||||
allowCodexRootKind: false,
|
||||
onClose: vi.fn(),
|
||||
onImported: vi.fn(),
|
||||
})
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const selects = host.querySelectorAll('select');
|
||||
const rootSelect = selects[1] as HTMLSelectElement;
|
||||
expect(Array.from(rootSelect.options).some((option) => option.value === 'codex')).toBe(false);
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { act } from 'react';
|
|||
import { createRoot } from 'react-dom/client';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { CliInstallationStatus } from '@shared/types';
|
||||
import type { SkillCatalogItem } from '@shared/types/extensions';
|
||||
|
||||
interface StoreState {
|
||||
|
|
@ -12,6 +13,7 @@ interface StoreState {
|
|||
skillsDetailsById: Record<string, unknown>;
|
||||
skillsUserCatalog: SkillCatalogItem[];
|
||||
skillsProjectCatalogByProjectPath: Record<string, SkillCatalogItem[]>;
|
||||
cliStatus: CliInstallationStatus | null;
|
||||
}
|
||||
|
||||
const storeState = {} as StoreState;
|
||||
|
|
@ -107,11 +109,19 @@ vi.mock('@renderer/components/extensions/skills/SkillDetailDialog', () => ({
|
|||
}));
|
||||
|
||||
vi.mock('@renderer/components/extensions/skills/SkillEditorDialog', () => ({
|
||||
SkillEditorDialog: () => null,
|
||||
SkillEditorDialog: ({ allowCodexRootKind }: { allowCodexRootKind: boolean }) =>
|
||||
React.createElement('div', {
|
||||
'data-testid': 'skill-editor-dialog',
|
||||
'data-allow-codex-root-kind': String(allowCodexRootKind),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@renderer/components/extensions/skills/SkillImportDialog', () => ({
|
||||
SkillImportDialog: () => null,
|
||||
SkillImportDialog: ({ allowCodexRootKind }: { allowCodexRootKind: boolean }) =>
|
||||
React.createElement('div', {
|
||||
'data-testid': 'skill-import-dialog',
|
||||
'data-allow-codex-root-kind': String(allowCodexRootKind),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('lucide-react', () => {
|
||||
|
|
@ -170,6 +180,22 @@ describe('SkillsPanel', () => {
|
|||
storeState.skillsProjectCatalogByProjectPath = {
|
||||
'/tmp/project-a': [],
|
||||
};
|
||||
storeState.cliStatus = {
|
||||
flavor: 'claude',
|
||||
displayName: 'Claude CLI',
|
||||
supportsSelfUpdate: true,
|
||||
showVersionDetails: true,
|
||||
showBinaryPath: true,
|
||||
installed: true,
|
||||
installedVersion: '1.0.0',
|
||||
binaryPath: '/usr/local/bin/claude',
|
||||
latestVersion: '1.0.0',
|
||||
updateAvailable: false,
|
||||
authLoggedIn: true,
|
||||
authStatusChecking: false,
|
||||
authMethod: 'oauth',
|
||||
providers: [],
|
||||
};
|
||||
startWatchingMock.mockReset();
|
||||
stopWatchingMock.mockReset();
|
||||
onChangedMock.mockReset();
|
||||
|
|
@ -232,4 +258,39 @@ describe('SkillsPanel', () => {
|
|||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it('hides codex-only create and import affordances when codex runtime is unavailable', async () => {
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
React.createElement(SkillsPanel, {
|
||||
projectPath: '/tmp/project-a',
|
||||
projectLabel: 'Project A',
|
||||
skillsSearchQuery: '',
|
||||
setSkillsSearchQuery: vi.fn(),
|
||||
skillsSort: 'name-asc',
|
||||
setSkillsSort: vi.fn(),
|
||||
selectedSkillId: null,
|
||||
setSelectedSkillId: vi.fn(),
|
||||
})
|
||||
);
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(host.textContent).not.toContain('Codex only');
|
||||
for (const node of host.querySelectorAll('[data-testid="skill-editor-dialog"]')) {
|
||||
expect(node.getAttribute('data-allow-codex-root-kind')).toBe('false');
|
||||
}
|
||||
const importDialog = host.querySelector('[data-testid="skill-import-dialog"]');
|
||||
expect(importDialog?.getAttribute('data-allow-codex-root-kind')).toBe('false');
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue