Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,6 @@ const registerAgentCombinedView = (context: vscode.ExtensionContext): vscode.Dis

try {
await provider.resetCurrentAgentView();
vscode.window.showInformationMessage('Agent preview reset.');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
vscode.window.showErrorMessage(`Unable to reset agent view: ${errorMessage}`);
Expand Down
4 changes: 1 addition & 3 deletions test/commands/refreshAgents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ describe('Refresh Agents Command', () => {

try {
await mockProvider.resetCurrentAgentView();
vscode.window.showInformationMessage('Agent preview reset.');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
vscode.window.showErrorMessage(`Unable to reset agent view: ${errorMessage}`);
Expand All @@ -74,7 +73,7 @@ describe('Refresh Agents Command', () => {
await resetCommand();

expect(mockProvider.resetCurrentAgentView).toHaveBeenCalled();
expect(vscode.window.showInformationMessage).toHaveBeenCalledWith('Agent preview reset.');
expect(vscode.window.showInformationMessage).not.toHaveBeenCalled();
});

it('should show an error when no agent is selected', async () => {
Expand Down Expand Up @@ -107,7 +106,6 @@ describe('Refresh Agents Command', () => {

try {
await mockProvider.resetCurrentAgentView();
vscode.window.showInformationMessage('Agent preview reset.');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
vscode.window.showErrorMessage(`Unable to reset agent view: ${errorMessage}`);
Expand Down
29 changes: 25 additions & 4 deletions test/services/coreExtensionService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ jest.mock('semver', () => ({
valid: jest.fn()
}));

// Mock @salesforce/core so the fallback path in getDefaultConnection
// doesn't reach into the host's ~/.sfdx config and a real Salesforce
// connection during tests.
jest.mock('@salesforce/core', () => ({
ConfigAggregator: {
create: jest.fn()
},
Org: {
create: jest.fn()
}
}));

import { ConfigAggregator, Org } from '@salesforce/core';

describe('CoreExtensionService', () => {
let mockExtension: { packageJSON: { version: string }; exports: CoreExtensionApi };
let mockContext: ExtensionContext;
Expand Down Expand Up @@ -284,10 +298,17 @@ describe('CoreExtensionService', () => {
});

it('should attempt to create connection directly when getting default connection before initialization', async () => {
// When not initialized, should try to create connection via ConfigAggregator
// This will fail in test environment without mocking, but error message will include
// both NOT_INITIALIZED_ERROR prefix and more specific info about what went wrong
await expect(CoreExtensionService.getDefaultConnection()).rejects.toThrow();
// When not initialized, the fallback path uses ConfigAggregator + Org to
// produce a connection. Force the fallback to fail and assert the error
// wraps NOT_INITIALIZED_ERROR with the underlying message.
(ConfigAggregator.create as jest.Mock).mockResolvedValue({
getPropertyValue: () => undefined
});

await expect(CoreExtensionService.getDefaultConnection()).rejects.toThrow(
new RegExp(`${NOT_INITIALIZED_ERROR}.*No default org configured`)
);
expect(Org.create).not.toHaveBeenCalled();
});

it('should not reinitialize if already initialized', async () => {
Expand Down
1 change: 1 addition & 0 deletions test/webview/AgentPreview.coverage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ describe('AgentPreview - Coverage Tests', () => {
isSessionTransitioning={false}
onSessionTransitionSettled={jest.fn()}
isLiveMode={false}
isSessionActive
{...props}
/>
);
Expand Down
31 changes: 30 additions & 1 deletion test/webview/AgentPreview.placeholder.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ describe('AgentPreview - Placeholder Behavior', () => {
});
});

it('should start session when placeholder button is clicked', async () => {
it('should start session via vscodeApi when placeholder button is clicked and no parent handler is provided', async () => {
const user = userEvent.setup();
render(
<AgentPreview
Expand All @@ -144,6 +144,35 @@ describe('AgentPreview - Placeholder Behavior', () => {

expect(mockVscodeApi.startSession).toHaveBeenCalledWith('test-agent');
});

it('should delegate placeholder start to the parent handler when one is provided', async () => {
const user = userEvent.setup();
const onStartSession = jest.fn();
render(
<AgentPreview
selectedAgentId="test-agent"
pendingAgentId={null}
isSessionTransitioning={false}
onSessionTransitionSettled={jest.fn()}
isLiveMode={false}
onStartSession={onStartSession}
/>
);

const noHistoryHandler = messageHandlers.get('noHistoryFound');
noHistoryHandler!({ agentId: 'test-agent' });

await waitFor(() => {
expect(screen.getByText('Start Simulation')).toBeInTheDocument();
});

await user.click(screen.getByText('Start Simulation'));

expect(onStartSession).toHaveBeenCalledTimes(1);
// When the parent handles it, AgentPreview should not call vscodeApi
// directly — the parent owns the session-transition lifecycle.
expect(mockVscodeApi.startSession).not.toHaveBeenCalled();
});
});

describe('Placeholder State Reset', () => {
Expand Down
92 changes: 90 additions & 2 deletions test/webview/AgentPreview.sendMessage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,24 @@ describe('AgentPreview send message guard', () => {

});

const renderPreview = () =>
render(
const renderPreview = (overrides: Partial<React.ComponentProps<typeof AgentPreview>> = {}) => {
const buildElement = (props: Partial<React.ComponentProps<typeof AgentPreview>>) => (
<AgentPreview
selectedAgentId="agent-one"
pendingAgentId={null}
isSessionTransitioning={false}
onSessionTransitionSettled={jest.fn()}
isLiveMode={false}
isSessionActive
{...props}
/>
);
const utils = render(buildElement(overrides));
return {
...utils,
update: (next: Partial<React.ComponentProps<typeof AgentPreview>>) => utils.rerender(buildElement(next))
};
};

it('does not send messages when agent is not connected', async () => {
renderPreview();
Expand Down Expand Up @@ -85,4 +93,84 @@ describe('AgentPreview send message guard', () => {

expect(mockVscodeApi.sendChatMessage).toHaveBeenCalledWith('Hello agent');
});

it('does not send messages when parent reports session as inactive', async () => {
renderPreview({ isSessionActive: false });

await waitFor(() => {
expect(formPropsRef.current?.onSendMessage).toBeDefined();
});

// Connect locally so agentConnected is true.
messageHandlers.get('sessionStarted')?.({ content: 'hello' });

formPropsRef.current.onSendMessage('Hello agent');

expect(mockVscodeApi.sendChatMessage).not.toHaveBeenCalled();
});

it('does not send messages while a stop is pending', async () => {
const { update } = renderPreview({ isSessionActive: true });

await waitFor(() => {
expect(formPropsRef.current?.onSendMessage).toBeDefined();
});

messageHandlers.get('sessionStarted')?.({ content: 'hello' });

await waitFor(() => {
expect(formPropsRef.current?.sessionActive).toBe(true);
});

// Simulate the parent flipping isStopPending after the user clicked Stop.
update({ isSessionActive: true, isStopPending: true });

formPropsRef.current.onSendMessage('Hello agent');

expect(mockVscodeApi.sendChatMessage).not.toHaveBeenCalled();
expect(formPropsRef.current?.sessionActive).toBe(false);
});

it('does not send messages while a session transition is in flight', async () => {
const { update } = renderPreview({ isSessionActive: true });

await waitFor(() => {
expect(formPropsRef.current?.onSendMessage).toBeDefined();
});

messageHandlers.get('sessionStarted')?.({ content: 'hello' });

await waitFor(() => {
expect(formPropsRef.current?.sessionActive).toBe(true);
});

update({ isSessionActive: true, isSessionTransitioning: true });

formPropsRef.current.onSendMessage('Hello agent');

expect(mockVscodeApi.sendChatMessage).not.toHaveBeenCalled();
expect(formPropsRef.current?.sessionActive).toBe(false);
});

it('disables the input the moment the parent reports session inactive after a stop', async () => {
const { update } = renderPreview({ isSessionActive: true });

await waitFor(() => {
expect(formPropsRef.current?.onSendMessage).toBeDefined();
});

messageHandlers.get('sessionStarted')?.({ content: 'hello' });

await waitFor(() => {
expect(formPropsRef.current?.sessionActive).toBe(true);
});

// App optimistically flips isSessionActive to false on Stop click,
// before the backend confirms with sessionEnded. The input should
// disable immediately.
update({ isSessionActive: false, isStopPending: true });

expect(formPropsRef.current?.sessionActive).toBe(false);
});

});
Loading
Loading