Fix so rewind starts at the bottom and loadHistory refreshes static content. (#17335)

This commit is contained in:
Jacob Richman
2026-01-22 12:54:23 -08:00
committed by GitHub
parent fda903587c
commit 6a78632873
10 changed files with 269 additions and 139 deletions
@@ -156,11 +156,28 @@ describe('useSlashCommandProcessor', () => {
});
const setupProcessorHook = async (
builtinCommands: SlashCommand[] = [],
fileCommands: SlashCommand[] = [],
mcpCommands: SlashCommand[] = [],
setIsProcessing = vi.fn(),
options: {
builtinCommands?: SlashCommand[];
fileCommands?: SlashCommand[];
mcpCommands?: SlashCommand[];
setIsProcessing?: (isProcessing: boolean) => void;
refreshStatic?: () => void;
openAgentConfigDialog?: (
name: string,
displayName: string,
definition: unknown,
) => void;
} = {},
) => {
const {
builtinCommands = [],
fileCommands = [],
mcpCommands = [],
setIsProcessing = vi.fn(),
refreshStatic = vi.fn(),
openAgentConfigDialog = vi.fn(),
} = options;
mockBuiltinLoadCommands.mockResolvedValue(Object.freeze(builtinCommands));
mockFileLoadCommands.mockResolvedValue(Object.freeze(fileCommands));
mockMcpLoadCommands.mockResolvedValue(Object.freeze(mcpCommands));
@@ -177,7 +194,7 @@ describe('useSlashCommandProcessor', () => {
mockAddItem,
mockClearItems,
mockLoadHistory,
vi.fn(), // refreshStatic
refreshStatic,
vi.fn(), // toggleVimEnabled
setIsProcessing,
{
@@ -188,7 +205,7 @@ describe('useSlashCommandProcessor', () => {
openSettingsDialog: vi.fn(),
openSessionBrowser: vi.fn(),
openModelDialog: mockOpenModelDialog,
openAgentConfigDialog: vi.fn(),
openAgentConfigDialog,
openPermissionsDialog: vi.fn(),
quit: mockSetQuittingMessages,
setDebugMessage: vi.fn(),
@@ -235,7 +252,9 @@ describe('useSlashCommandProcessor', () => {
context.ui.clear();
},
});
const result = await setupProcessorHook([clearCommand]);
const result = await setupProcessorHook({
builtinCommands: [clearCommand],
});
await act(async () => {
await result.current.handleSlashCommand('/clear');
@@ -252,7 +271,9 @@ describe('useSlashCommandProcessor', () => {
context.ui.clear();
},
});
const result = await setupProcessorHook([clearCommand]);
const result = await setupProcessorHook({
builtinCommands: [clearCommand],
});
await act(async () => {
await result.current.handleSlashCommand('/clear');
@@ -272,7 +293,9 @@ describe('useSlashCommandProcessor', () => {
it('should call loadCommands and populate state after mounting', async () => {
const testCommand = createTestCommand({ name: 'test' });
const result = await setupProcessorHook([testCommand]);
const result = await setupProcessorHook({
builtinCommands: [testCommand],
});
await waitFor(() => {
expect(result.current.slashCommands).toHaveLength(1);
@@ -286,7 +309,9 @@ describe('useSlashCommandProcessor', () => {
it('should provide an immutable array of commands to consumers', async () => {
const testCommand = createTestCommand({ name: 'test' });
const result = await setupProcessorHook([testCommand]);
const result = await setupProcessorHook({
builtinCommands: [testCommand],
});
await waitFor(() => {
expect(result.current.slashCommands).toHaveLength(1);
@@ -314,7 +339,10 @@ describe('useSlashCommandProcessor', () => {
CommandKind.FILE,
);
const result = await setupProcessorHook([builtinCommand], [fileCommand]);
const result = await setupProcessorHook({
builtinCommands: [builtinCommand],
fileCommands: [fileCommand],
});
await waitFor(() => {
// The service should only return one command with the name 'override'
@@ -364,7 +392,9 @@ describe('useSlashCommandProcessor', () => {
},
],
};
const result = await setupProcessorHook([parentCommand]);
const result = await setupProcessorHook({
builtinCommands: [parentCommand],
});
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
await act(async () => {
@@ -398,7 +428,9 @@ describe('useSlashCommandProcessor', () => {
},
],
};
const result = await setupProcessorHook([parentCommand]);
const result = await setupProcessorHook({
builtinCommands: [parentCommand],
});
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
await act(async () => {
@@ -422,7 +454,9 @@ describe('useSlashCommandProcessor', () => {
it('sets isProcessing to false if the the input is not a command', async () => {
const setMockIsProcessing = vi.fn();
const result = await setupProcessorHook([], [], [], setMockIsProcessing);
const result = await setupProcessorHook({
setIsProcessing: setMockIsProcessing,
});
await act(async () => {
await result.current.handleSlashCommand('imnotacommand');
@@ -438,12 +472,10 @@ describe('useSlashCommandProcessor', () => {
action: vi.fn().mockRejectedValue(new Error('oh no!')),
});
const result = await setupProcessorHook(
[failCommand],
[],
[],
setMockIsProcessing,
);
const result = await setupProcessorHook({
builtinCommands: [failCommand],
setIsProcessing: setMockIsProcessing,
});
await waitFor(() => expect(result.current.slashCommands).toBeDefined());
@@ -462,12 +494,10 @@ describe('useSlashCommandProcessor', () => {
action: () => new Promise((resolve) => setTimeout(resolve, 50)),
});
const result = await setupProcessorHook(
[command],
[],
[],
mockSetIsProcessing,
);
const result = await setupProcessorHook({
builtinCommands: [command],
setIsProcessing: mockSetIsProcessing,
});
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
const executionPromise = act(async () => {
@@ -509,7 +539,9 @@ describe('useSlashCommandProcessor', () => {
.fn()
.mockResolvedValue({ type: 'dialog', dialog: dialogType }),
});
const result = await setupProcessorHook([command]);
const result = await setupProcessorHook({
builtinCommands: [command],
});
await waitFor(() =>
expect(result.current.slashCommands).toHaveLength(1),
);
@@ -539,48 +571,9 @@ describe('useSlashCommandProcessor', () => {
}),
});
// Re-setup the hook with the mock action that we can inspect
mockBuiltinLoadCommands.mockResolvedValue(Object.freeze([command]));
mockFileLoadCommands.mockResolvedValue(Object.freeze([]));
mockMcpLoadCommands.mockResolvedValue(Object.freeze([]));
let result!: { current: ReturnType<typeof useSlashCommandProcessor> };
await act(async () => {
const hook = renderHook(() =>
useSlashCommandProcessor(
mockConfig,
mockSettings,
mockAddItem,
mockClearItems,
mockLoadHistory,
vi.fn(),
vi.fn(),
vi.fn(),
{
openAuthDialog: vi.fn(),
openThemeDialog: vi.fn(),
openEditorDialog: vi.fn(),
openPrivacyNotice: vi.fn(),
openSettingsDialog: vi.fn(),
openSessionBrowser: vi.fn(),
openModelDialog: vi.fn(),
openAgentConfigDialog: mockOpenAgentConfigDialog,
openPermissionsDialog: vi.fn(),
quit: vi.fn(),
setDebugMessage: vi.fn(),
toggleCorgiMode: vi.fn(),
toggleDebugProfiler: vi.fn(),
dispatchExtensionStateUpdate: vi.fn(),
addConfirmUpdateExtensionRequest: vi.fn(),
setText: vi.fn(),
},
new Map(),
true,
vi.fn(),
vi.fn(),
),
);
result = hook.result;
const result = await setupProcessorHook({
builtinCommands: [command],
openAgentConfigDialog: mockOpenAgentConfigDialog,
});
await waitFor(() =>
@@ -614,20 +607,42 @@ describe('useSlashCommandProcessor', () => {
clientHistory: [{ role: 'user', parts: [{ text: 'old prompt' }] }],
}),
});
const result = await setupProcessorHook([command]);
const mockRefreshStatic = vi.fn();
const result = await setupProcessorHook({
builtinCommands: [command],
refreshStatic: mockRefreshStatic,
});
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
await act(async () => {
await result.current.handleSlashCommand('/load');
});
// ui.clear() is called which calls refreshStatic()
expect(mockClearItems).toHaveBeenCalledTimes(1);
expect(mockRefreshStatic).toHaveBeenCalledTimes(1);
expect(mockAddItem).toHaveBeenCalledWith(
{ type: 'user', text: 'old prompt' },
expect.any(Number),
);
});
it('should call refreshStatic exactly once when ui.loadHistory is called', async () => {
const mockRefreshStatic = vi.fn();
const result = await setupProcessorHook({
refreshStatic: mockRefreshStatic,
});
await act(async () => {
result.current.commandContext.ui.loadHistory([]);
});
expect(mockLoadHistory).toHaveBeenCalled();
expect(mockRefreshStatic).toHaveBeenCalledTimes(1);
});
it('should handle a "quit" action', async () => {
const quitAction = vi
.fn()
@@ -636,7 +651,9 @@ describe('useSlashCommandProcessor', () => {
name: 'exit',
action: quitAction,
});
const result = await setupProcessorHook([command]);
const result = await setupProcessorHook({
builtinCommands: [command],
});
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
@@ -659,7 +676,9 @@ describe('useSlashCommandProcessor', () => {
CommandKind.FILE,
);
const result = await setupProcessorHook([], [fileCommand]);
const result = await setupProcessorHook({
fileCommands: [fileCommand],
});
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
let actionResult;
@@ -691,7 +710,9 @@ describe('useSlashCommandProcessor', () => {
CommandKind.MCP_PROMPT,
);
const result = await setupProcessorHook([], [], [mcpCommand]);
const result = await setupProcessorHook({
mcpCommands: [mcpCommand],
});
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
let actionResult;
@@ -714,7 +735,9 @@ describe('useSlashCommandProcessor', () => {
describe('Command Parsing and Matching', () => {
it('should be case-sensitive', async () => {
const command = createTestCommand({ name: 'test' });
const result = await setupProcessorHook([command]);
const result = await setupProcessorHook({
builtinCommands: [command],
});
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
await act(async () => {
@@ -740,7 +763,9 @@ describe('useSlashCommandProcessor', () => {
description: 'a command with an alias',
action,
});
const result = await setupProcessorHook([command]);
const result = await setupProcessorHook({
builtinCommands: [command],
});
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
await act(async () => {
@@ -756,7 +781,9 @@ describe('useSlashCommandProcessor', () => {
it('should handle extra whitespace around the command', async () => {
const action = vi.fn();
const command = createTestCommand({ name: 'test', action });
const result = await setupProcessorHook([command]);
const result = await setupProcessorHook({
builtinCommands: [command],
});
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
await act(async () => {
@@ -769,7 +796,9 @@ describe('useSlashCommandProcessor', () => {
it('should handle `?` as a command prefix', async () => {
const action = vi.fn();
const command = createTestCommand({ name: 'help', action });
const result = await setupProcessorHook([command]);
const result = await setupProcessorHook({
builtinCommands: [command],
});
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
await act(async () => {
@@ -798,7 +827,10 @@ describe('useSlashCommandProcessor', () => {
CommandKind.FILE,
);
const result = await setupProcessorHook([], [fileCommand], [mcpCommand]);
const result = await setupProcessorHook({
fileCommands: [fileCommand],
mcpCommands: [mcpCommand],
});
await waitFor(() => {
// The service should only return one command with the name 'override'
@@ -834,7 +866,10 @@ describe('useSlashCommandProcessor', () => {
// The order of commands in the final loaded array is not guaranteed,
// so the test must work regardless of which comes first.
const result = await setupProcessorHook([quitCommand], [exitCommand]);
const result = await setupProcessorHook({
builtinCommands: [quitCommand],
fileCommands: [exitCommand],
});
await waitFor(() => {
expect(result.current.slashCommands).toHaveLength(2);
@@ -861,7 +896,10 @@ describe('useSlashCommandProcessor', () => {
CommandKind.FILE,
);
const result = await setupProcessorHook([quitCommand], [exitCommand]);
const result = await setupProcessorHook({
builtinCommands: [quitCommand],
fileCommands: [exitCommand],
});
await waitFor(() => expect(result.current.slashCommands).toHaveLength(2));
await act(async () => {
@@ -957,7 +995,9 @@ describe('useSlashCommandProcessor', () => {
desc: 'command path when alias is used',
},
])('should log $desc', async ({ command, expectedLog }) => {
const result = await setupProcessorHook(loggingTestCommands);
const result = await setupProcessorHook({
builtinCommands: loggingTestCommands,
});
await waitFor(() => expect(result.current.slashCommands).toBeDefined());
await act(async () => {
@@ -976,7 +1016,9 @@ describe('useSlashCommandProcessor', () => {
{ command: '/bogusbogusbogus', desc: 'bogus command' },
{ command: '/unknown', desc: 'unknown command' },
])('should not log for $desc', async ({ command }) => {
const result = await setupProcessorHook(loggingTestCommands);
const result = await setupProcessorHook({
builtinCommands: loggingTestCommands,
});
await waitFor(() => expect(result.current.slashCommands).toBeDefined());
await act(async () => {