Skip to content

Commit 55a981a

Browse files
rekmarksclaude
andcommitted
fix(kernel-ui): validate KRef target at state-entry and improve cast safety
Validate target with isKRef() in the select onChange handler instead of at send time, making invalid state unrepresentable. Add test for invalid target rejection and clarifying comments on safe regex-guarded as-casts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9cf9682 commit 55a981a

3 files changed

Lines changed: 26 additions & 7 deletions

File tree

packages/kernel-ui/src/components/SendMessageForm.test.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { useRegistry } from '../hooks/useRegistry.ts';
1717
import type { ObjectRegistry } from '../types.ts';
1818
import { SendMessageForm } from './SendMessageForm.tsx';
1919

20-
setupOcapKernelMock();
20+
const { setMockBehavior } = setupOcapKernelMock();
2121

2222
vi.mock('../context/PanelContext.tsx', () => ({
2323
usePanelContext: vi.fn(),
@@ -193,6 +193,20 @@ describe('SendMessageForm Component', () => {
193193
expect(sendButton).toBeDisabled();
194194
});
195195

196+
it('does not set target when value fails KRef validation', async () => {
197+
setMockBehavior({ isKRef: false });
198+
const { getByTestId } = render(<SendMessageForm />);
199+
200+
const targetSelect = getByTestId('message-target');
201+
fireEvent.change(targetSelect, { target: { value: 'not-a-kref' } });
202+
203+
// Target should remain unset, button stays disabled
204+
expect(targetSelect).toHaveValue('');
205+
expect(getByTestId('message-send-button')).toBeDisabled();
206+
207+
setMockBehavior({ isKRef: true });
208+
});
209+
196210
it('calls callKernelMethod with correct parameters when Send button is clicked', async () => {
197211
const { getByTestId } = render(<SendMessageForm />);
198212

packages/kernel-ui/src/components/SendMessageForm.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from '@metamask/design-system-react';
1111
import { stringify } from '@metamask/kernel-utils';
1212
import { isKRef } from '@metamask/ocap-kernel';
13+
import type { KRef } from '@metamask/ocap-kernel';
1314
import type { Json } from '@metamask/utils';
1415
import { useState, useMemo } from 'react';
1516

@@ -27,7 +28,7 @@ const inputStyle =
2728
export const SendMessageForm: React.FC = () => {
2829
const { callKernelMethod, logMessage, objectRegistry } = usePanelContext();
2930
const { fetchObjectRegistry } = useRegistry();
30-
const [target, setTarget] = useState('');
31+
const [target, setTarget] = useState<KRef | null>(null);
3132
const [method, setMethod] = useState('__getMethodNames__');
3233
const [paramsText, setParamsText] = useState('[]');
3334
const [result, setResult] = useState<Json | null>(null);
@@ -64,8 +65,7 @@ export const SendMessageForm: React.FC = () => {
6465
}, [objectRegistry]);
6566

6667
const handleSend = (): void => {
67-
if (!isKRef(target)) {
68-
logMessage('Invalid target', 'error');
68+
if (!target) {
6969
return;
7070
}
7171
Promise.resolve()
@@ -121,8 +121,11 @@ export const SendMessageForm: React.FC = () => {
121121
</label>
122122
<select
123123
id="message-target"
124-
value={target}
125-
onChange={(event) => setTarget(event.target.value)}
124+
value={target ?? ''}
125+
onChange={(event) => {
126+
const { value } = event.target;
127+
setTarget(isKRef(value) ? value : null);
128+
}}
126129
data-testid="message-target"
127130
className={`${inputStyle} cursor-pointer`}
128131
>
@@ -176,7 +179,7 @@ export const SendMessageForm: React.FC = () => {
176179
variant={ButtonVariant.Primary}
177180
size={ButtonSize.Sm}
178181
onClick={handleSend}
179-
isDisabled={!(target.trim() && method.trim())}
182+
isDisabled={!(target && method.trim())}
180183
className="h-9 rounded-md"
181184
data-testid="message-send-button"
182185
>

packages/kernel-ui/src/services/db-parser.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export function parseObjectRegistry(
8181
matches[1] &&
8282
objCList.push({
8383
vat: matches[1],
84+
// Safe cast: regex capture group guarantees KRef format
8485
kref: matches[2] as KRef,
8586
eref: value.replace(/^R\s*/u, ''),
8687
});
@@ -90,6 +91,7 @@ export function parseObjectRegistry(
9091
matches[1] &&
9192
prmCList.push({
9293
vat: matches[1],
94+
// Safe cast: regex capture group guarantees KRef format
9395
kref: matches[2] as KRef,
9496
eref: value.replace(/^R\s*/u, ''),
9597
});

0 commit comments

Comments
 (0)