Skip to content

Commit 521272f

Browse files
authored
feat: Send messages to vats from the control panel and remove sendVatCommand (#508)
Closes #489 With this PR - the `sendVatCommand` kernel method is removed - we now send messages to vats through the `queueMessage` kernel command (it was renamed from `queueMessageFromKernel`) from a form located at Object Registry tab - In anticipation of #490 a placeholder for the `ping` call was preserved in the UI. - Added/fixed several tests ## Preview <img width="1197" alt="Screenshot 2025-05-02 at 17 45 56" src="https://github.com/user-attachments/assets/07458cf4-4e43-49bd-a4b0-e9e53851ebbb" /> https://github.com/user-attachments/assets/368fe553-3e66-4fdb-991f-0ec2ca463ee7
1 parent a9c6b08 commit 521272f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1390
-874
lines changed

packages/extension/src/kernel-integration/handlers/index.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,9 @@ import {
99
} from './execute-db-query.ts';
1010
import { getStatusHandler, getStatusSpec } from './get-status.ts';
1111
import { launchVatHandler, launchVatSpec } from './launch-vat.ts';
12+
import { queueMessageHandler, queueMessageSpec } from './queue-message.ts';
1213
import { reloadConfigHandler, reloadConfigSpec } from './reload-config.ts';
1314
import { restartVatHandler, restartVatSpec } from './restart-vat.ts';
14-
import {
15-
sendVatCommandHandler,
16-
sendVatCommandSpec,
17-
} from './send-vat-command.ts';
1815
import {
1916
terminateAllVatsHandler,
2017
terminateAllVatsSpec,
@@ -35,7 +32,7 @@ export const handlers = {
3532
launchVat: launchVatHandler,
3633
reload: reloadConfigHandler,
3734
restartVat: restartVatHandler,
38-
sendVatCommand: sendVatCommandHandler,
35+
queueMessage: queueMessageHandler,
3936
terminateAllVats: terminateAllVatsHandler,
4037
collectGarbage: collectGarbageHandler,
4138
terminateVat: terminateVatHandler,
@@ -52,7 +49,7 @@ export const methodSpecs = {
5249
launchVat: launchVatSpec,
5350
reload: reloadConfigSpec,
5451
restartVat: restartVatSpec,
55-
sendVatCommand: sendVatCommandSpec,
52+
queueMessage: queueMessageSpec,
5653
terminateAllVats: terminateAllVatsSpec,
5754
collectGarbage: collectGarbageSpec,
5855
terminateVat: terminateVatSpec,
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import type { CapData } from '@endo/marshal';
2+
import type { Kernel } from '@metamask/ocap-kernel';
3+
import { describe, it, expect, vi, beforeEach } from 'vitest';
4+
5+
import { queueMessageSpec, queueMessageHandler } from './queue-message.ts';
6+
7+
describe('queueMessageSpec', () => {
8+
it('should define the correct method name', () => {
9+
expect(queueMessageSpec.method).toBe('queueMessage');
10+
});
11+
12+
it('should define the correct parameter structure', () => {
13+
// Valid parameters should pass validation
14+
const validParams = [
15+
'target123',
16+
'methodName',
17+
[1, 'string', { key: 'value' }],
18+
];
19+
expect(() => queueMessageSpec.params.create(validParams)).not.toThrow();
20+
21+
// Invalid parameters should fail validation
22+
const invalidParams = ['target123', 123, [1, 'string']];
23+
expect(() => queueMessageSpec.params.create(invalidParams)).toThrow(
24+
'Expected a string',
25+
);
26+
});
27+
28+
it('should define the correct result structure', () => {
29+
// Valid result should pass validation
30+
const validResult: CapData<string> = { body: 'result', slots: [] };
31+
expect(() => queueMessageSpec.result.create(validResult)).not.toThrow();
32+
33+
// Invalid result should fail validation
34+
const invalidResult = 'not a CapData object';
35+
expect(() => queueMessageSpec.result.create(invalidResult)).toThrow(
36+
'Expected an object',
37+
);
38+
});
39+
});
40+
41+
describe('queueMessageHandler', () => {
42+
let mockKernel: Pick<Kernel, 'queueMessage'>;
43+
44+
beforeEach(() => {
45+
mockKernel = {
46+
queueMessage: vi.fn(),
47+
};
48+
});
49+
50+
it('should correctly forward arguments to kernel.queueMessage', async () => {
51+
const target = 'targetId';
52+
const method = 'methodName';
53+
const args = [1, 'string', { key: 'value' }];
54+
const expectedResult: CapData<string> = { body: 'result', slots: [] };
55+
56+
vi.mocked(mockKernel.queueMessage).mockResolvedValueOnce(expectedResult);
57+
58+
const result = await queueMessageHandler.implementation(
59+
{ kernel: mockKernel },
60+
[target, method, args],
61+
);
62+
63+
expect(mockKernel.queueMessage).toHaveBeenCalledWith(target, method, args);
64+
expect(result).toStrictEqual(expectedResult);
65+
});
66+
67+
it('should propagate errors from kernel.queueMessage', async () => {
68+
const error = new Error('Queue message failed');
69+
vi.mocked(mockKernel.queueMessage).mockRejectedValueOnce(error);
70+
71+
await expect(
72+
queueMessageHandler.implementation({ kernel: mockKernel }, [
73+
'target',
74+
'method',
75+
[],
76+
]),
77+
).rejects.toThrow('Queue message failed');
78+
});
79+
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { CapData } from '@endo/marshal';
2+
import type { MethodSpec, Handler } from '@metamask/kernel-rpc-methods';
3+
import type { Kernel } from '@metamask/ocap-kernel';
4+
import { CapDataStruct } from '@metamask/ocap-kernel';
5+
import { tuple, string, array } from '@metamask/superstruct';
6+
import { UnsafeJsonStruct } from '@metamask/utils';
7+
import type { Json } from '@metamask/utils';
8+
9+
/**
10+
* Enqueue a message to a vat via the kernel's crank queue.
11+
*/
12+
export const queueMessageSpec: MethodSpec<
13+
'queueMessage',
14+
[string, string, Json[]],
15+
CapData<string>
16+
> = {
17+
method: 'queueMessage',
18+
params: tuple([string(), string(), array(UnsafeJsonStruct)]),
19+
result: CapDataStruct,
20+
};
21+
22+
export type QueueMessageHooks = {
23+
kernel: Pick<Kernel, 'queueMessage'>;
24+
};
25+
26+
export const queueMessageHandler: Handler<
27+
'queueMessage',
28+
[string, string, Json[]],
29+
Promise<CapData<string>>,
30+
QueueMessageHooks
31+
> = {
32+
...queueMessageSpec,
33+
hooks: { kernel: true },
34+
implementation: async (
35+
{ kernel }: QueueMessageHooks,
36+
[target, method, args],
37+
): Promise<CapData<string>> => {
38+
return kernel.queueMessage(target, method, args);
39+
},
40+
};

packages/extension/src/kernel-integration/handlers/send-vat-command.test.ts

Lines changed: 0 additions & 61 deletions
This file was deleted.

packages/extension/src/kernel-integration/handlers/send-vat-command.ts

Lines changed: 0 additions & 50 deletions
This file was deleted.

packages/extension/src/ui/App.module.css

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,21 @@ body > div {
4949
box-sizing: border-box;
5050
}
5151

52+
h1,
53+
h2,
54+
h3,
55+
h4,
56+
h5,
57+
h6 {
58+
font-weight: 600;
59+
}
60+
61+
pre {
62+
word-break: break-word;
63+
white-space: normal;
64+
line-height: 1.5;
65+
}
66+
5267
/* Panel container */
5368
.panel {
5469
padding: var(--spacing-xl);
@@ -65,8 +80,10 @@ body > div {
6580
}
6681

6782
/* Common form elements */
83+
input,
6884
.input,
6985
.button,
86+
select,
7087
.select {
7188
height: var(--input-height);
7289
padding: 0 var(--spacing-lg);
@@ -156,6 +173,11 @@ select,
156173
color: var(--color-white);
157174
}
158175

176+
.buttonBlack:hover:not(:disabled) {
177+
background-color: var(--color-gray-600);
178+
color: var(--color-white);
179+
}
180+
159181
.textButton {
160182
padding: 0;
161183
border: 0;
@@ -200,12 +222,6 @@ select,
200222
margin-bottom: var(--spacing-sm);
201223
}
202224

203-
.messageInputRow {
204-
display: flex;
205-
gap: var(--spacing-sm);
206-
margin-bottom: var(--spacing-sm);
207-
}
208-
209225
.messageContent {
210226
composes: input;
211227
flex: 1;
@@ -338,16 +354,45 @@ div + .sent {
338354
}
339355

340356
.messageInputSection {
341-
border-top: 1px solid var(--color-gray-300);
357+
border: 1px solid var(--color-gray-300);
342358
padding: var(--spacing-md);
343359
background: var(--color-gray-200);
360+
border-radius: var(--border-radius);
361+
margin-bottom: var(--spacing-xl);
362+
}
363+
364+
.messageInputSection h3 {
365+
margin: 0 0 var(--spacing-md);
344366
}
345367

346-
.messageInputRow {
368+
.horizontalForm {
347369
display: flex;
348370
gap: var(--spacing-sm);
349371
}
350372

373+
.horizontalForm > div {
374+
display: flex;
375+
flex-direction: column;
376+
flex: 1;
377+
}
378+
379+
.horizontalForm > div > label {
380+
margin-bottom: var(--spacing-xs);
381+
}
382+
383+
.messageResponse {
384+
font-family: monospace;
385+
font-size: var(--font-size-xs);
386+
}
387+
388+
.messageResponse h4 {
389+
margin: var(--spacing-md) 0 var(--spacing-sm);
390+
}
391+
392+
.messageResponse pre {
393+
margin: 0;
394+
}
395+
351396
.table {
352397
width: 100%;
353398
border: 1px solid var(--color-gray-300);
@@ -598,3 +643,10 @@ table.table {
598643
font-weight: 400;
599644
margin-left: var(--spacing-xs);
600645
}
646+
647+
@media (min-width: 1200px) {
648+
.horizontalForm .formFieldTarget {
649+
width: 150px;
650+
flex: none;
651+
}
652+
}

packages/extension/src/ui/components/ConfigEditor.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ const mockUsePanelContext = {
3333
setMessageContent: vi.fn(),
3434
setSelectedVatId: vi.fn(),
3535
status: mockStatus,
36+
objectRegistry: null,
37+
setObjectRegistry: vi.fn(),
3638
};
3739

3840
vi.mock('../hooks/useKernelActions.ts', () => ({
@@ -70,7 +72,6 @@ describe('ConfigEditor Component', () => {
7072
vi.mocked(useKernelActions).mockReturnValue({
7173
updateClusterConfig: mockUpdateClusterConfig,
7274
reload: mockReload,
73-
sendKernelCommand: vi.fn(),
7475
terminateAllVats: vi.fn(),
7576
clearState: vi.fn(),
7677
launchVat: vi.fn(),

0 commit comments

Comments
 (0)