Skip to content

Commit fe24315

Browse files
callmeYeclaude
andcommitted
fix(skills): revert dep-array misread + add test coverage for review
Review comment 3303143705 pointed at `buffer,` in the useMemo dependency array, not the object literal. `buffer` is a correct dep (the useMemo callback references `buffer.setText`). The object at line 3489 already has the proper `setInputBuffer: buffer.setText`. No source change needed — reply will clarify. Test coverage (review comments 3304330911/17/23/27): - useSelectionList.test.ts: `describe('disableVimNav')` — bare j/k suppression, ctrl+n pass-through, arrow-key navigation. - slashCommandProcessor.test.ts: reload skipped when `consumeSlashReloadSuppression()` returns true. - skill.test.ts (core): commandExecutor throws in disabled branch → graceful fallback to disabled-error message. - config.integration.test.ts: `buildDisabledSkillNamesProvider` unit tests covering normal arrays, non-array inputs, mixed-type arrays, whitespace trimming, and empty-after-trim filtering. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 42e5b06 commit fe24315

4 files changed

Lines changed: 183 additions & 0 deletions

File tree

packages/cli/src/config/config.integration.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,3 +415,50 @@ describe('Configuration Integration Tests', () => {
415415
});
416416
});
417417
});
418+
419+
describe('buildDisabledSkillNamesProvider', async () => {
420+
const { buildDisabledSkillNamesProvider } = await import('./config.js');
421+
422+
function fakeSettings(disabled: unknown) {
423+
return { merged: { skills: { disabled } } } as never;
424+
}
425+
426+
it('returns a normalized set from a normal array', () => {
427+
const provider = buildDisabledSkillNamesProvider(
428+
fakeSettings(['Foo', ' BAR ', 'baz']),
429+
);
430+
const result = provider();
431+
expect(result).toEqual(new Set(['foo', 'bar', 'baz']));
432+
});
433+
434+
it('returns empty set for non-array values (string)', () => {
435+
const provider = buildDisabledSkillNamesProvider(fakeSettings('all'));
436+
expect(provider()).toEqual(new Set());
437+
});
438+
439+
it('returns empty set for non-array values (number)', () => {
440+
const provider = buildDisabledSkillNamesProvider(fakeSettings(42));
441+
expect(provider()).toEqual(new Set());
442+
});
443+
444+
it('returns empty set for null/undefined', () => {
445+
const provider = buildDisabledSkillNamesProvider(fakeSettings(null));
446+
expect(provider()).toEqual(new Set());
447+
const provider2 = buildDisabledSkillNamesProvider(fakeSettings(undefined));
448+
expect(provider2()).toEqual(new Set());
449+
});
450+
451+
it('filters non-string elements from a mixed-type array', () => {
452+
const provider = buildDisabledSkillNamesProvider(
453+
fakeSettings([42, null, 'valid', undefined, true, ' TRIMMED ']),
454+
);
455+
expect(provider()).toEqual(new Set(['valid', 'trimmed']));
456+
});
457+
458+
it('excludes empty-after-trim strings', () => {
459+
const provider = buildDisabledSkillNamesProvider(
460+
fakeSettings([' ', '', 'keep']),
461+
);
462+
expect(provider()).toEqual(new Set(['keep']));
463+
});
464+
});

packages/cli/src/ui/hooks/slashCommandProcessor.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,6 +1102,65 @@ describe('useSlashCommandProcessor', () => {
11021102
}
11031103
});
11041104

1105+
it('should skip reload when consumeSlashReloadSuppression returns true', async () => {
1106+
const removeListener = vi.fn();
1107+
const addChangeListener = vi.fn().mockReturnValue(removeListener);
1108+
const fakeSkillManager = {
1109+
addChangeListener,
1110+
consumeSlashReloadSuppression: vi.fn(() => true),
1111+
};
1112+
const skillManagerSpy = vi
1113+
.spyOn(mockConfig, 'getSkillManager')
1114+
.mockReturnValue(
1115+
fakeSkillManager as unknown as ReturnType<
1116+
typeof mockConfig.getSkillManager
1117+
>,
1118+
);
1119+
try {
1120+
mockBuiltinLoadCommands.mockResolvedValue([]);
1121+
mockFileLoadCommands.mockResolvedValue([]);
1122+
mockMcpLoadCommands.mockResolvedValue([]);
1123+
1124+
const { unmount } = renderHook(() =>
1125+
useSlashCommandProcessor(
1126+
mockConfig,
1127+
mockSettings,
1128+
mockAddItem,
1129+
mockClearItems,
1130+
mockLoadHistory,
1131+
vi.fn(),
1132+
vi.fn(),
1133+
false,
1134+
vi.fn(),
1135+
{ current: true },
1136+
vi.fn(),
1137+
createMockActions(),
1138+
new Map(),
1139+
true,
1140+
null,
1141+
),
1142+
);
1143+
1144+
await waitFor(() => expect(addChangeListener).toHaveBeenCalledTimes(1));
1145+
await waitFor(() =>
1146+
expect(BuiltinCommandLoader).toHaveBeenCalledTimes(1),
1147+
);
1148+
1149+
const listener = addChangeListener.mock.calls[0][0] as () => void;
1150+
await act(async () => {
1151+
listener();
1152+
});
1153+
1154+
// When suppression is consumed, the listener should NOT trigger
1155+
// a second load — BuiltinCommandLoader stays at 1 call.
1156+
expect(BuiltinCommandLoader).toHaveBeenCalledTimes(1);
1157+
1158+
unmount();
1159+
} finally {
1160+
skillManagerSpy.mockRestore();
1161+
}
1162+
});
1163+
11051164
it('should register SkillManager listener after config initialization', async () => {
11061165
const removeListener = vi.fn();
11071166
const addChangeListener = vi.fn().mockReturnValue(removeListener);

packages/cli/src/ui/hooks/useSelectionList.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,4 +1027,61 @@ describe('useSelectionList', () => {
10271027
expect(mockOnSelect).not.toHaveBeenCalled();
10281028
});
10291029
});
1030+
1031+
describe('disableVimNav', () => {
1032+
it('bare j does NOT dispatch MOVE_DOWN when disableVimNav is true', () => {
1033+
const { result } = renderHook(() =>
1034+
useSelectionList({
1035+
items,
1036+
onSelect: mockOnSelect,
1037+
disableVimNav: true,
1038+
}),
1039+
);
1040+
expect(result.current.activeIndex).toBe(0);
1041+
pressKey('j');
1042+
expect(result.current.activeIndex).toBe(0);
1043+
});
1044+
1045+
it('bare k does NOT dispatch MOVE_UP when disableVimNav is true', () => {
1046+
const { result } = renderHook(() =>
1047+
useSelectionList({
1048+
items,
1049+
onSelect: mockOnSelect,
1050+
initialIndex: 2,
1051+
disableVimNav: true,
1052+
}),
1053+
);
1054+
expect(result.current.activeIndex).toBe(2);
1055+
pressKey('k');
1056+
expect(result.current.activeIndex).toBe(2);
1057+
});
1058+
1059+
it('Ctrl+N still dispatches MOVE_DOWN when disableVimNav is true', () => {
1060+
const { result } = renderHook(() =>
1061+
useSelectionList({
1062+
items,
1063+
onSelect: mockOnSelect,
1064+
disableVimNav: true,
1065+
}),
1066+
);
1067+
expect(result.current.activeIndex).toBe(0);
1068+
pressKey('n', 'n', { ctrl: true });
1069+
expect(result.current.activeIndex).toBe(2);
1070+
});
1071+
1072+
it('arrow keys still work when disableVimNav is true', () => {
1073+
const { result } = renderHook(() =>
1074+
useSelectionList({
1075+
items,
1076+
onSelect: mockOnSelect,
1077+
disableVimNav: true,
1078+
}),
1079+
);
1080+
expect(result.current.activeIndex).toBe(0);
1081+
pressKey('down');
1082+
expect(result.current.activeIndex).toBe(2);
1083+
pressKey('up');
1084+
expect(result.current.activeIndex).toBe(0);
1085+
});
1086+
});
10301087
});

packages/core/src/tools/skill.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,26 @@ describe('SkillTool', () => {
928928
expect(llmText).toMatch(/is disabled/);
929929
});
930930

931+
it('falls through to disabled-error when commandExecutor throws', async () => {
932+
vi.mocked(config.getDisabledSkillNames).mockReturnValue(
933+
new Set(['mytool']),
934+
);
935+
const executor = vi.fn().mockRejectedValue(new Error('MCP timeout'));
936+
vi.mocked(config.getModelInvocableCommandsExecutor).mockReturnValue(
937+
executor,
938+
);
939+
940+
const invocation = (
941+
skillTool as SkillToolWithProtectedMethods
942+
).createInvocation({ skill: 'mytool' });
943+
const result = await invocation.execute();
944+
945+
expect(executor).toHaveBeenCalledWith('mytool');
946+
expect(mockSkillManager.loadSkillForRuntime).not.toHaveBeenCalled();
947+
const llmText = partToString(result.llmContent);
948+
expect(llmText).toMatch(/is disabled/);
949+
});
950+
931951
it('does not affect a skill that is not disabled', async () => {
932952
// Sanity check: with skills.disabled empty, the original
933953
// loadSkillForRuntime → executor fallback ordering still applies.

0 commit comments

Comments
 (0)