diff --git a/packages/extension/src/kernel-integration/handlers/index.test.ts b/packages/extension/src/kernel-integration/handlers/index.test.ts new file mode 100644 index 000000000..3cad01983 --- /dev/null +++ b/packages/extension/src/kernel-integration/handlers/index.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect } from 'vitest'; + +import { clearStateHandler, clearStateSpec } from './clear-state.ts'; +import { + collectGarbageHandler, + collectGarbageSpec, +} from './collect-garbage.ts'; +import { + executeDBQueryHandler, + executeDBQuerySpec, +} from './execute-db-query.ts'; +import { getStatusHandler, getStatusSpec } from './get-status.ts'; +import { handlers, methodSpecs } from './index.ts'; +import { launchVatHandler, launchVatSpec } from './launch-vat.ts'; +import { pingVatHandler, pingVatSpec } from './ping-vat.ts'; +import { queueMessageHandler, queueMessageSpec } from './queue-message.ts'; +import { reloadConfigHandler, reloadConfigSpec } from './reload-config.ts'; +import { restartVatHandler, restartVatSpec } from './restart-vat.ts'; +import { + terminateAllVatsHandler, + terminateAllVatsSpec, +} from './terminate-all-vats.ts'; +import { terminateVatHandler, terminateVatSpec } from './terminate-vat.ts'; +import { + updateClusterConfigHandler, + updateClusterConfigSpec, +} from './update-cluster-config.ts'; + +describe('handlers/index', () => { + it('should export all handler functions', () => { + expect(handlers).toStrictEqual({ + clearState: clearStateHandler, + executeDBQuery: executeDBQueryHandler, + getStatus: getStatusHandler, + launchVat: launchVatHandler, + pingVat: pingVatHandler, + reload: reloadConfigHandler, + restartVat: restartVatHandler, + queueMessage: queueMessageHandler, + terminateAllVats: terminateAllVatsHandler, + collectGarbage: collectGarbageHandler, + terminateVat: terminateVatHandler, + updateClusterConfig: updateClusterConfigHandler, + }); + }); + + it('should have all handlers with the correct method property', () => { + const handlerEntries = Object.entries(handlers); + + handlerEntries.forEach(([key, handler]) => { + expect(handler).toHaveProperty('method'); + expect(handler.method).toBe(key); + }); + }); + + it('should export all method specs', () => { + expect(methodSpecs).toStrictEqual({ + clearState: clearStateSpec, + executeDBQuery: executeDBQuerySpec, + getStatus: getStatusSpec, + launchVat: launchVatSpec, + pingVat: pingVatSpec, + reload: reloadConfigSpec, + restartVat: restartVatSpec, + queueMessage: queueMessageSpec, + terminateAllVats: terminateAllVatsSpec, + collectGarbage: collectGarbageSpec, + terminateVat: terminateVatSpec, + updateClusterConfig: updateClusterConfigSpec, + }); + }); + + it('should have the same keys as handlers', () => { + expect(Object.keys(methodSpecs).sort()).toStrictEqual( + Object.keys(handlers).sort(), + ); + }); +}); diff --git a/packages/extension/src/kernel-integration/handlers/index.ts b/packages/extension/src/kernel-integration/handlers/index.ts index 5558ca679..ea072c5bf 100644 --- a/packages/extension/src/kernel-integration/handlers/index.ts +++ b/packages/extension/src/kernel-integration/handlers/index.ts @@ -9,6 +9,7 @@ import { } from './execute-db-query.ts'; import { getStatusHandler, getStatusSpec } from './get-status.ts'; import { launchVatHandler, launchVatSpec } from './launch-vat.ts'; +import { pingVatHandler, pingVatSpec } from './ping-vat.ts'; import { queueMessageHandler, queueMessageSpec } from './queue-message.ts'; import { reloadConfigHandler, reloadConfigSpec } from './reload-config.ts'; import { restartVatHandler, restartVatSpec } from './restart-vat.ts'; @@ -30,6 +31,7 @@ export const handlers = { executeDBQuery: executeDBQueryHandler, getStatus: getStatusHandler, launchVat: launchVatHandler, + pingVat: pingVatHandler, reload: reloadConfigHandler, restartVat: restartVatHandler, queueMessage: queueMessageHandler, @@ -47,6 +49,7 @@ export const methodSpecs = { executeDBQuery: executeDBQuerySpec, getStatus: getStatusSpec, launchVat: launchVatSpec, + pingVat: pingVatSpec, reload: reloadConfigSpec, restartVat: restartVatSpec, queueMessage: queueMessageSpec, diff --git a/packages/extension/src/kernel-integration/handlers/ping-vat.test.ts b/packages/extension/src/kernel-integration/handlers/ping-vat.test.ts new file mode 100644 index 000000000..5e24afabb --- /dev/null +++ b/packages/extension/src/kernel-integration/handlers/ping-vat.test.ts @@ -0,0 +1,35 @@ +import type { Kernel } from '@metamask/ocap-kernel'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import { pingVatHandler } from './ping-vat.ts'; + +describe('pingVatHandler', () => { + let mockKernel: Kernel; + + beforeEach(() => { + mockKernel = { + pingVat: vi.fn().mockResolvedValue('pong'), + } as unknown as Kernel; + }); + + it('should ping vat and return result', async () => { + const params = { id: 'v0' } as const; + const result = await pingVatHandler.implementation( + { kernel: mockKernel }, + params, + ); + + expect(mockKernel.pingVat).toHaveBeenCalledWith(params.id); + expect(result).toBe('pong'); + }); + + it('should propagate errors from kernel.pingVat', async () => { + const error = new Error('Ping failed'); + vi.mocked(mockKernel.pingVat).mockRejectedValueOnce(error); + + const params = { id: 'v0' } as const; + await expect( + pingVatHandler.implementation({ kernel: mockKernel }, params), + ).rejects.toThrow(error); + }); +}); diff --git a/packages/extension/src/kernel-integration/handlers/ping-vat.ts b/packages/extension/src/kernel-integration/handlers/ping-vat.ts new file mode 100644 index 000000000..36455ba8d --- /dev/null +++ b/packages/extension/src/kernel-integration/handlers/ping-vat.ts @@ -0,0 +1,29 @@ +import type { Handler, MethodSpec } from '@metamask/kernel-rpc-methods'; +import type { Kernel, VatId } from '@metamask/ocap-kernel'; +import { VatIdStruct } from '@metamask/ocap-kernel'; +import { vatMethodSpecs } from '@metamask/ocap-kernel/rpc'; +import type { PingVatResult } from '@metamask/ocap-kernel/rpc'; +import { object } from '@metamask/superstruct'; + +export type PingVatHooks = { + kernel: Kernel; +}; + +export const pingVatSpec: MethodSpec<'pingVat', { id: VatId }, string> = { + method: 'pingVat', + params: object({ id: VatIdStruct }), + result: vatMethodSpecs.ping.result, +}; + +export const pingVatHandler: Handler< + 'pingVat', + { id: VatId }, + Promise, + PingVatHooks +> = { + ...pingVatSpec, + hooks: { kernel: true }, + implementation: async ({ kernel }, params): Promise => { + return kernel.pingVat(params.id); + }, +}; diff --git a/packages/extension/src/kernel-integration/middleware/panel-message.test.ts b/packages/extension/src/kernel-integration/middleware/panel-message.test.ts index 1d0a336dd..2a2d7eff2 100644 --- a/packages/extension/src/kernel-integration/middleware/panel-message.test.ts +++ b/packages/extension/src/kernel-integration/middleware/panel-message.test.ts @@ -1,6 +1,6 @@ import { JsonRpcEngine } from '@metamask/json-rpc-engine'; import type { KernelDatabase } from '@metamask/kernel-store'; -import type { Kernel } from '@metamask/ocap-kernel'; +import type { ClusterConfig, Kernel } from '@metamask/ocap-kernel'; import type { JsonRpcRequest } from '@metamask/utils'; import { describe, it, expect, vi, beforeEach } from 'vitest'; @@ -13,9 +13,42 @@ const { mockAssertHasMethod, mockExecute } = vi.hoisted(() => ({ vi.mock('@metamask/kernel-rpc-methods', () => ({ RpcService: class MockRpcService { + readonly #dependencies: Record; + + constructor( + _handlers: Record, + dependencies: Record, + ) { + this.#dependencies = dependencies; + } + assertHasMethod = mockAssertHasMethod; - execute = mockExecute; + execute = (method: string, params: unknown) => { + // For updateClusterConfig test, call the actual implementation + if (method === 'updateClusterConfig' && params) { + const updateFn = this.#dependencies.updateClusterConfig as ( + config: unknown, + ) => void; + updateFn(params); + return Promise.resolve(); + } + + // For executeDBQuery test, call the actual implementation + if ( + method === 'executeDBQuery' && + typeof params === 'object' && + params !== null + ) { + const { sql } = params as { sql: string }; + const executeQueryFn = this.#dependencies.executeDBQuery as ( + sql: string, + ) => Promise; + return executeQueryFn(sql); + } + + return mockExecute(method, params); + }; }, })); @@ -23,6 +56,8 @@ vi.mock('../handlers/index.ts', () => ({ handlers: { testMethod1: { method: 'testMethod1' }, testMethod2: { method: 'testMethod2' }, + updateClusterConfig: { method: 'updateClusterConfig' }, + executeDBQuery: { method: 'executeDBQuery' }, }, })); @@ -33,8 +68,12 @@ describe('createPanelMessageMiddleware', () => { beforeEach(() => { // Set up mocks - mockKernel = {} as Kernel; - mockKernelDatabase = {} as KernelDatabase; + mockKernel = { + clusterConfig: {} as ClusterConfig, + } as Kernel; + mockKernelDatabase = { + executeQuery: vi.fn(), + } as unknown as KernelDatabase; // Create a new JSON-RPC engine with our middleware engine = new JsonRpcEngine(); @@ -209,4 +248,63 @@ describe('createPanelMessageMiddleware', () => { }), }); }); + + it('should update kernel.clusterConfig when updateClusterConfig is called', async () => { + // Create a test cluster config that matches the expected structure + const testConfig = { + bootstrap: 'test-bootstrap', + vats: { + test: { + bundleSpec: 'test-bundle', + }, + }, + forceReset: true, + } as ClusterConfig; + + // Create a request to update cluster config + const request = { + id: 7, + jsonrpc: '2.0', + method: 'updateClusterConfig', + params: testConfig, + } as JsonRpcRequest; + + // Process the request + await engine.handle(request); + + // Verify that kernel.clusterConfig was updated with the provided config + expect(mockKernel.clusterConfig).toStrictEqual(testConfig); + }); + + it('should call kernelDatabase.executeQuery when executeDBQuery is called', async () => { + // Set up mock database response + const mockQueryResult = [{ id: '1', name: 'test' }]; + vi.mocked(mockKernelDatabase.executeQuery).mockResolvedValueOnce( + mockQueryResult, + ); + + // Test SQL query + const testSql = 'SELECT * FROM test_table'; + + // Create a request to execute DB query + const request = { + id: 8, + jsonrpc: '2.0', + method: 'executeDBQuery', + params: { sql: testSql }, + } as JsonRpcRequest; + + // Process the request + const response = await engine.handle(request); + + // Verify that kernelDatabase.executeQuery was called with the correct SQL + expect(mockKernelDatabase.executeQuery).toHaveBeenCalledWith(testSql); + + // Verify the response contains the query result + expect(response).toStrictEqual({ + id: 8, + jsonrpc: '2.0', + result: mockQueryResult, + }); + }); }); diff --git a/packages/extension/src/ui/hooks/useVats.test.ts b/packages/extension/src/ui/hooks/useVats.test.ts index e6e883bc4..c19f10300 100644 --- a/packages/extension/src/ui/hooks/useVats.test.ts +++ b/packages/extension/src/ui/hooks/useVats.test.ts @@ -83,6 +83,100 @@ describe('useVats', () => { ]); }); + it('should use sourceSpec when bundleSpec is not available', async () => { + const { usePanelContext } = await import('../context/PanelContext.tsx'); + const sourceSpecValue = 'source-test'; + vi.mocked(usePanelContext).mockReturnValue({ + callKernelMethod: mockSendMessage, + status: { + vats: [ + { + id: mockVatId, + config: { + sourceSpec: sourceSpecValue, + parameters: { foo: 'bar' }, + creationOptions: { test: true }, + } as VatConfig, + }, + ], + }, + selectedVatId: mockVatId, + setSelectedVatId: mockSetSelectedVatId, + logMessage: mockLogMessage, + } as unknown as PanelContextType); + const { useVats } = await import('./useVats.ts'); + const { result } = renderHook(() => useVats()); + expect(result.current.vats).toStrictEqual([ + { + id: mockVatId, + source: sourceSpecValue, + parameters: '{"foo":"bar"}', + creationOptions: '{"test":true}', + }, + ]); + }); + + it('should use bundleName when bundleSpec and sourceSpec are not available', async () => { + const { usePanelContext } = await import('../context/PanelContext.tsx'); + const bundleNameValue = 'bundle-name-test'; + vi.mocked(usePanelContext).mockReturnValue({ + callKernelMethod: mockSendMessage, + status: { + vats: [ + { + id: mockVatId, + config: { + bundleName: bundleNameValue, + parameters: { foo: 'bar' }, + creationOptions: { test: true }, + } as VatConfig, + }, + ], + }, + selectedVatId: mockVatId, + setSelectedVatId: mockSetSelectedVatId, + logMessage: mockLogMessage, + } as unknown as PanelContextType); + const { useVats } = await import('./useVats.ts'); + const { result } = renderHook(() => useVats()); + expect(result.current.vats).toStrictEqual([ + { + id: mockVatId, + source: bundleNameValue, + parameters: '{"foo":"bar"}', + creationOptions: '{"test":true}', + }, + ]); + }); + + describe('pingVat', () => { + it('should send ping message and log success', async () => { + const { useVats } = await import('./useVats.ts'); + const { result } = renderHook(() => useVats()); + const pingResult = 'pong'; + mockSendMessage.mockResolvedValueOnce(pingResult); + result.current.pingVat(mockVatId); + await waitFor(() => { + expect(mockSendMessage).toHaveBeenCalledWith({ + method: 'pingVat', + params: { id: mockVatId }, + }); + }); + expect(mockLogMessage).toHaveBeenCalledWith(pingResult, 'success'); + }); + + it('should handle ping error', async () => { + const { useVats } = await import('./useVats.ts'); + const { result } = renderHook(() => useVats()); + const errorMessage = 'Vat not responding'; + mockSendMessage.mockRejectedValueOnce(new Error(errorMessage)); + result.current.pingVat(mockVatId); + await waitFor(() => { + expect(mockLogMessage).toHaveBeenCalledWith(errorMessage, 'error'); + }); + }); + }); + describe('restartVat', () => { it('should send restart message and log success', async () => { const { useVats } = await import('./useVats.ts'); diff --git a/packages/extension/src/ui/hooks/useVats.ts b/packages/extension/src/ui/hooks/useVats.ts index db2113e94..25d260acd 100644 --- a/packages/extension/src/ui/hooks/useVats.ts +++ b/packages/extension/src/ui/hooks/useVats.ts @@ -47,14 +47,14 @@ export const useVats = (): { */ const pingVat = useCallback( (id: VatId) => { - // TODO: Implement ping - new Promise((_resolve, reject) => { - reject(new Error(`Cannot ping vat ${id}: Ping is not implemented`)); + callKernelMethod({ + method: 'pingVat', + params: { id }, }) - .then((result) => logMessage(stringify(result, 0), 'received')) + .then((result) => logMessage(result, 'success')) .catch((error) => logMessage(error.message, 'error')); }, - [logMessage], + [callKernelMethod, logMessage], ); /** diff --git a/packages/ocap-kernel/src/Kernel.test.ts b/packages/ocap-kernel/src/Kernel.test.ts index 2e9851272..e2666520e 100644 --- a/packages/ocap-kernel/src/Kernel.test.ts +++ b/packages/ocap-kernel/src/Kernel.test.ts @@ -97,6 +97,7 @@ describe('Kernel', () => { deliverMessage: vi.fn(), deliverNotify: vi.fn(), sendVatCommand: vi.fn(), + ping: vi.fn(), } as unknown as VatHandle; vatHandles.push(vatHandle as Mocked); return vatHandle; @@ -550,74 +551,136 @@ describe('Kernel', () => { expect(kernel.getVatIds()).toStrictEqual([]); }); - it('returns the existing VatHandle instance on restart', async () => { - const kernel = await Kernel.make( - mockStream, - mockWorkerService, - mockKernelDatabase, - ); - await kernel.launchVat(makeMockVatConfig()); - const originalHandle = vatHandles[0]; - const returnedHandle = await kernel.restartVat('v1'); - expect(returnedHandle).toBe(originalHandle); + describe('pingVat()', () => { + it('pings a vat without errors when the vat exists', async () => { + const kernel = await Kernel.make( + mockStream, + mockWorkerService, + mockKernelDatabase, + ); + await kernel.launchVat(makeMockVatConfig()); + vatHandles[0]?.ping.mockResolvedValueOnce('pong'); + const result = await kernel.pingVat('v1'); + expect(vatHandles[0]?.ping).toHaveBeenCalledTimes(1); + expect(result).toBe('pong'); + }); + + it('throws an error when pinging a vat that does not exist in the kernel', async () => { + const kernel = await Kernel.make( + mockStream, + mockWorkerService, + mockKernelDatabase, + ); + const nonExistentVatId: VatId = 'v9'; + await expect(async () => + kernel.pingVat(nonExistentVatId), + ).rejects.toThrow(VatNotFoundError); + }); + + it('propagates errors from the vat ping method', async () => { + const kernel = await Kernel.make( + mockStream, + mockWorkerService, + mockKernelDatabase, + ); + await kernel.launchVat(makeMockVatConfig()); + const pingError = new Error('Ping failed'); + vatHandles[0]?.ping.mockRejectedValueOnce(pingError); + await expect(async () => kernel.pingVat('v1')).rejects.toThrow( + pingError, + ); + }); }); - }); - describe('clusterConfig', () => { - it('gets and sets cluster configuration', async () => { - const kernel = await Kernel.make( - mockStream, - mockWorkerService, - mockKernelDatabase, - ); - expect(kernel.clusterConfig).toBeNull(); - const config = makeMockClusterConfig(); - kernel.clusterConfig = config; - expect(kernel.clusterConfig).toStrictEqual(config); + describe('constructor()', () => { + it('initializes the kernel without errors', async () => { + expect( + async () => + await Kernel.make( + mockStream, + mockWorkerService, + mockKernelDatabase, + ), + ).not.toThrow(); + }); }); - it('throws an error when setting invalid config', async () => { - const kernel = await Kernel.make( - mockStream, - mockWorkerService, - mockKernelDatabase, - ); - expect(() => { - // @ts-expect-error Intentionally setting invalid config - kernel.clusterConfig = { invalid: true }; - }).toThrow('invalid cluster config'); + describe('init()', () => { + it.todo('initializes the kernel store'); + it.todo('starts receiving messages'); + it.todo('throws if the stream throws'); }); - }); - describe('reset()', () => { - it('terminates all vats and resets kernel state', async () => { - const mockDb = makeMapKernelDatabase(); - const clearSpy = vi.spyOn(mockDb, 'clear'); - const kernel = await Kernel.make(mockStream, mockWorkerService, mockDb); - await kernel.launchVat(makeMockVatConfig()); - await kernel.reset(); - expect(clearSpy).toHaveBeenCalled(); - expect(kernel.getVatIds()).toHaveLength(0); + describe('reload()', () => { + it('should reload with current config when config exists', async () => { + const kernel = await Kernel.make( + mockStream, + mockWorkerService, + mockKernelDatabase, + ); + await kernel.launchVat(makeMockVatConfig()); + const originalHandle = vatHandles[0]; + const returnedHandle = await kernel.restartVat('v1'); + expect(returnedHandle).toBe(originalHandle); + }); }); - }); - describe('pinVatRoot and unpinVatRoot', () => { - it('pins and unpins a vat root correctly', async () => { - const kernel = await Kernel.make( - mockStream, - mockWorkerService, - mockKernelDatabase, - ); - const config = makeMockVatConfig(); - const rootRef = await kernel.launchVat(config); - // Pinning existing vat root should return the kref - expect(kernel.pinVatRoot('v1')).toBe(rootRef); - // Pinning non-existent vat should throw - expect(() => kernel.pinVatRoot('v2')).toThrow(VatNotFoundError); - // Unpinning existing vat root should succeed - expect(() => kernel.unpinVatRoot('v1')).not.toThrow(); - // Unpinning non-existent vat should throw - expect(() => kernel.unpinVatRoot('v3')).toThrow(VatNotFoundError); + describe('clusterConfig', () => { + it('gets and sets cluster configuration', async () => { + const kernel = await Kernel.make( + mockStream, + mockWorkerService, + mockKernelDatabase, + ); + expect(kernel.clusterConfig).toBeNull(); + const config = makeMockClusterConfig(); + kernel.clusterConfig = config; + expect(kernel.clusterConfig).toStrictEqual(config); + }); + + it('throws an error when setting invalid config', async () => { + const kernel = await Kernel.make( + mockStream, + mockWorkerService, + mockKernelDatabase, + ); + expect(() => { + // @ts-expect-error Intentionally setting invalid config + kernel.clusterConfig = { invalid: true }; + }).toThrow('invalid cluster config'); + }); + }); + + describe('reset()', () => { + it('terminates all vats and resets kernel state', async () => { + const mockDb = makeMapKernelDatabase(); + const clearSpy = vi.spyOn(mockDb, 'clear'); + const kernel = await Kernel.make(mockStream, mockWorkerService, mockDb); + await kernel.launchVat(makeMockVatConfig()); + await kernel.reset(); + expect(clearSpy).toHaveBeenCalled(); + expect(kernel.getVatIds()).toHaveLength(0); + }); + }); + + describe('pinVatRoot and unpinVatRoot', () => { + it('pins and unpins a vat root correctly', async () => { + const kernel = await Kernel.make( + mockStream, + mockWorkerService, + mockKernelDatabase, + ); + const config = makeMockVatConfig(); + const rootRef = await kernel.launchVat(config); + // Pinning existing vat root should return the kref + expect(kernel.pinVatRoot('v1')).toBe(rootRef); + // Pinning non-existent vat should throw + expect(() => kernel.pinVatRoot('v2')).toThrow(VatNotFoundError); + // Unpinning existing vat root should succeed + expect(() => kernel.unpinVatRoot('v1')).not.toThrow(); + // Unpinning non-existent vat should throw + expect(() => kernel.unpinVatRoot('v3')).toThrow(VatNotFoundError); + }); }); }); }); diff --git a/packages/ocap-kernel/src/Kernel.ts b/packages/ocap-kernel/src/Kernel.ts index dbc4960bc..1408ae27c 100644 --- a/packages/ocap-kernel/src/Kernel.ts +++ b/packages/ocap-kernel/src/Kernel.ts @@ -17,6 +17,7 @@ import type { JsonRpcResponse } from '@metamask/utils'; import { KernelQueue } from './KernelQueue.ts'; import { KernelRouter } from './KernelRouter.ts'; import { kernelHandlers } from './rpc/index.ts'; +import type { PingVatResult } from './rpc/index.ts'; import type { SlotValue } from './services/kernel-marshal.ts'; import { kslot } from './services/kernel-marshal.ts'; import { makeKernelStore } from './store/index.ts'; @@ -430,6 +431,17 @@ export class Kernel { this.#kernelStore.unpinObject(kref); } + /** + * Ping a vat. + * + * @param vatId - The ID of the vat. + * @returns A promise that resolves to the result of the ping. + */ + async pingVat(vatId: VatId): Promise { + const vat = this.#getVat(vatId); + return vat.ping(); + } + /** * Reset the kernel state. * This is for debugging purposes only. diff --git a/packages/ocap-kernel/src/VatHandle.test.ts b/packages/ocap-kernel/src/VatHandle.test.ts index ec4091ffa..d87fed225 100644 --- a/packages/ocap-kernel/src/VatHandle.test.ts +++ b/packages/ocap-kernel/src/VatHandle.test.ts @@ -1,3 +1,5 @@ +import type { VatOneResolution } from '@agoric/swingset-liveslots'; +import type { VatCheckpoint } from '@metamask/kernel-store'; import type { JsonRpcMessage } from '@metamask/kernel-utils'; import { isJsonRpcMessage } from '@metamask/kernel-utils'; import type { Logger } from '@metamask/logger'; @@ -10,6 +12,7 @@ import type { MockInstance } from 'vitest'; import type { KernelQueue } from './KernelQueue.ts'; import { makeKernelStore } from './store/index.ts'; import type { KernelStore } from './store/index.ts'; +import type { VRef, Message } from './types.ts'; import { VatHandle } from './VatHandle.ts'; import { makeMapKernelDatabase } from '../test/storage.ts'; @@ -93,6 +96,130 @@ describe('VatHandle', () => { }); }); + describe('ping', () => { + it('calls sendVatCommand with the correct method and params', async () => { + const { vat } = await makeVat(); + sendVatCommandMock.mockReset(); + sendVatCommandMock.mockResolvedValueOnce('pong'); + const result = await vat.ping(); + expect(sendVatCommandMock).toHaveBeenCalledTimes(1); + expect(sendVatCommandMock).toHaveBeenCalledWith({ + method: 'ping', + params: [], + }); + expect(result).toBe('pong'); + }); + + it('propagates errors from sendVatCommand', async () => { + const { vat } = await makeVat(); + sendVatCommandMock.mockReset(); + const error = new Error('Ping failed'); + sendVatCommandMock.mockRejectedValueOnce(error); + await expect(vat.ping()).rejects.toThrow('Ping failed'); + }); + }); + + describe('deliverMessage', () => { + it('calls sendVatCommand with the correct method and params', async () => { + const { vat } = await makeVat(); + sendVatCommandMock.mockReset(); + const mockCheckpoint: VatCheckpoint = [[], []]; + sendVatCommandMock.mockResolvedValueOnce(mockCheckpoint); + const target = 'kp1' as VRef; + const message: Message = { + methargs: { body: '["arg1","arg2"]', slots: [] }, + result: 'kp123', + }; + await vat.deliverMessage(target, message); + expect(sendVatCommandMock).toHaveBeenCalledTimes(1); + expect(sendVatCommandMock).toHaveBeenCalledWith({ + method: 'deliver', + params: ['message', target, message], + }); + }); + }); + + describe('deliverNotify', () => { + it('calls sendVatCommand with the correct method and params', async () => { + const { vat } = await makeVat(); + sendVatCommandMock.mockReset(); + const mockCheckpoint: VatCheckpoint = [[], []]; + sendVatCommandMock.mockResolvedValueOnce(mockCheckpoint); + const resolutions: VatOneResolution[] = [ + ['vp123', false, { body: '"resolved value"', slots: [] }], + ]; + await vat.deliverNotify(resolutions); + expect(sendVatCommandMock).toHaveBeenCalledTimes(1); + expect(sendVatCommandMock).toHaveBeenCalledWith({ + method: 'deliver', + params: ['notify', resolutions], + }); + }); + }); + + describe('deliverDropExports', () => { + it('calls sendVatCommand with the correct method and params', async () => { + const { vat } = await makeVat(); + sendVatCommandMock.mockReset(); + const mockCheckpoint: VatCheckpoint = [[], []]; + sendVatCommandMock.mockResolvedValueOnce(mockCheckpoint); + const vrefs: VRef[] = ['kp123', 'kp456']; + await vat.deliverDropExports(vrefs); + expect(sendVatCommandMock).toHaveBeenCalledTimes(1); + expect(sendVatCommandMock).toHaveBeenCalledWith({ + method: 'deliver', + params: ['dropExports', vrefs], + }); + }); + }); + + describe('deliverRetireExports', () => { + it('calls sendVatCommand with the correct method and params', async () => { + const { vat } = await makeVat(); + sendVatCommandMock.mockReset(); + const mockCheckpoint: VatCheckpoint = [[], []]; + sendVatCommandMock.mockResolvedValueOnce(mockCheckpoint); + const vrefs: VRef[] = ['kp123', 'kp456']; + await vat.deliverRetireExports(vrefs); + expect(sendVatCommandMock).toHaveBeenCalledTimes(1); + expect(sendVatCommandMock).toHaveBeenCalledWith({ + method: 'deliver', + params: ['retireExports', vrefs], + }); + }); + }); + + describe('deliverRetireImports', () => { + it('calls sendVatCommand with the correct method and params', async () => { + const { vat } = await makeVat(); + sendVatCommandMock.mockReset(); + const mockCheckpoint: VatCheckpoint = [[], []]; + sendVatCommandMock.mockResolvedValueOnce(mockCheckpoint); + const vrefs: VRef[] = ['kp123', 'kp456']; + await vat.deliverRetireImports(vrefs); + expect(sendVatCommandMock).toHaveBeenCalledTimes(1); + expect(sendVatCommandMock).toHaveBeenCalledWith({ + method: 'deliver', + params: ['retireImports', vrefs], + }); + }); + }); + + describe('deliverBringOutYourDead', () => { + it('calls sendVatCommand with the correct method and params', async () => { + const { vat } = await makeVat(); + sendVatCommandMock.mockReset(); + const mockCheckpoint: VatCheckpoint = [[], []]; + sendVatCommandMock.mockResolvedValueOnce(mockCheckpoint); + await vat.deliverBringOutYourDead(); + expect(sendVatCommandMock).toHaveBeenCalledTimes(1); + expect(sendVatCommandMock).toHaveBeenCalledWith({ + method: 'deliver', + params: ['bringOutYourDead'], + }); + }); + }); + describe('sendVatCommand', () => { it('sends a message and resolves the promise', async () => { const dispatch = vi.fn(); diff --git a/packages/ocap-kernel/src/VatHandle.ts b/packages/ocap-kernel/src/VatHandle.ts index 349947cf2..641133d79 100644 --- a/packages/ocap-kernel/src/VatHandle.ts +++ b/packages/ocap-kernel/src/VatHandle.ts @@ -17,7 +17,7 @@ import { isJsonRpcRequest, isJsonRpcResponse } from '@metamask/utils'; import type { KernelQueue } from './KernelQueue.ts'; import { vatMethodSpecs, vatSyscallHandlers } from './rpc/index.ts'; -import type { VatMethod } from './rpc/index.ts'; +import type { PingVatResult, VatMethod } from './rpc/index.ts'; import { kser } from './services/kernel-marshal.ts'; import type { KernelStore } from './store/index.ts'; import type { Message, VatId, VatConfig, VRef } from './types.ts'; @@ -153,6 +153,18 @@ export class VatHandle { }); } + /** + * Ping the vat. + * + * @returns A promise that resolves to the result of the ping. + */ + async ping(): Promise { + return await this.sendVatCommand({ + method: 'ping', + params: [], + }); + } + /** * Handle a message from the vat. * diff --git a/packages/ocap-kernel/src/rpc/vat/index.ts b/packages/ocap-kernel/src/rpc/vat/index.ts index 73a153bfa..a7aa1a01a 100644 --- a/packages/ocap-kernel/src/rpc/vat/index.ts +++ b/packages/ocap-kernel/src/rpc/vat/index.ts @@ -1,3 +1,5 @@ +import type { Infer } from '@metamask/superstruct'; + import { deliverSpec, deliverHandler } from './deliver.ts'; import type { DeliverSpec, DeliverHandler } from './deliver.ts'; import { initVatSpec, initVatHandler } from './initVat.ts'; @@ -31,3 +33,5 @@ export const vatMethodSpecs = { type Handlers = (typeof vatHandlers)[keyof typeof vatHandlers]; export type VatMethod = Handlers['method']; + +export type PingVatResult = Infer; diff --git a/vitest.config.ts b/vitest.config.ts index 8dd0f24ea..1fa2c14a1 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -80,10 +80,10 @@ export default defineConfig({ lines: 100, }, 'packages/extension/**': { - statements: 83.61, - functions: 82.96, - branches: 79.12, - lines: 83.59, + statements: 85.05, + functions: 85.58, + branches: 79.85, + lines: 85.04, }, 'packages/kernel-errors/**': { statements: 98.63, @@ -128,10 +128,10 @@ export default defineConfig({ lines: 73.58, }, 'packages/ocap-kernel/**': { - statements: 91.48, - functions: 94.5, + statements: 91.57, + functions: 94.94, branches: 81.89, - lines: 91.46, + lines: 91.55, }, 'packages/streams/**': { statements: 100,