diff --git a/package.json b/package.json index e15a17c89..c9638d809 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "postinstall": "simple-git-hooks", "prepack": "./scripts/prepack.sh", "test": "vitest run", + "test:dev": "vitest run --mode development", "test:clean": "yarn test --no-cache --coverage.clean", "test:e2e": "yarn workspaces foreach --all run test:e2e", "test:e2e:ci": "yarn workspaces foreach --all run test:e2e:ci", diff --git a/packages/cli/package.json b/packages/cli/package.json index 8952fb734..98fd9b63e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -37,6 +37,7 @@ "@endo/init": "^1.1.6", "@endo/promise-kit": "^1.1.6", "@metamask/snaps-utils": "^8.3.0", + "@metamask/utils": "^11.3.0", "@ocap/shims": "workspace:^", "@ocap/utils": "workspace:^", "@types/node": "^22.13.1", @@ -51,7 +52,6 @@ "@metamask/eslint-config": "^14.0.0", "@metamask/eslint-config-nodejs": "^14.0.0", "@metamask/eslint-config-typescript": "^14.0.0", - "@metamask/utils": "^11.3.0", "@ocap/test-utils": "workspace:^", "@ts-bridge/cli": "^0.6.2", "@ts-bridge/shims": "^0.1.1", diff --git a/packages/extension/package.json b/packages/extension/package.json index 829b9fc1e..cb1788610 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -44,6 +44,7 @@ "@endo/eventual-send": "^1.2.6", "@endo/marshal": "^1.6.2", "@endo/promise-kit": "^1.1.6", + "@metamask/json-rpc-engine": "^10.0.3", "@metamask/snaps-utils": "^8.3.0", "@metamask/superstruct": "^3.1.0", "@metamask/utils": "^11.3.0", diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts index 3dd60613e..31fb08f0f 100644 --- a/packages/extension/src/background.ts +++ b/packages/extension/src/background.ts @@ -42,7 +42,7 @@ async function main(): Promise { value: async () => sendClusterCommand({ method: KernelCommandMethod.ping, - params: null, + params: [], }), }, sendMessage: { @@ -55,7 +55,7 @@ async function main(): Promise { chrome.action.onClicked.addListener(() => { sendClusterCommand({ method: KernelCommandMethod.ping, - params: null, + params: [], }).catch(console.error); }); diff --git a/packages/extension/src/kernel-integration/command-registry.test.ts b/packages/extension/src/kernel-integration/command-registry.test.ts new file mode 100644 index 000000000..cfe9373c8 --- /dev/null +++ b/packages/extension/src/kernel-integration/command-registry.test.ts @@ -0,0 +1,275 @@ +import type { Kernel, KernelCommand, VatId, VatConfig } from '@ocap/kernel'; +import type { KernelDatabase } from '@ocap/store'; +import { setupOcapKernelMock } from '@ocap/test-utils'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import { KernelCommandRegistry } from './command-registry.ts'; +import type { CommandHandler } from './command-registry.ts'; +import { handlers } from './handlers/index.ts'; + +// Mock logger +vi.mock('@ocap/utils', async (importOriginal) => ({ + ...(await importOriginal()), + makeLogger: () => ({ + error: vi.fn(), + debug: vi.fn(), + }), +})); + +const { setMockBehavior, resetMocks } = setupOcapKernelMock(); + +describe('KernelCommandRegistry', () => { + let registry: KernelCommandRegistry; + let mockKernel: Kernel; + let mockKernelDatabase: KernelDatabase; + + beforeEach(() => { + vi.resetModules(); + resetMocks(); + + mockKernelDatabase = { + kernelKVStore: { + get: vi.fn(), + getRequired: vi.fn(), + getNextKey: vi.fn(), + set: vi.fn(), + delete: vi.fn(), + }, + clear: vi.fn(), + executeQuery: vi.fn(), + makeVatStore: vi.fn(), + }; + + // Create mock kernel + mockKernel = { + launchVat: vi.fn().mockResolvedValue(undefined), + restartVat: vi.fn().mockResolvedValue(undefined), + terminateVat: vi.fn().mockResolvedValue(undefined), + terminateAllVats: vi.fn().mockResolvedValue(undefined), + clearStorage: vi.fn().mockResolvedValue(undefined), + getVatIds: vi.fn().mockReturnValue(['v0', 'v1']), + getVats: vi.fn().mockReturnValue([ + { + id: 'v0', + config: { bundleSpec: 'http://localhost:3000/sample-vat.bundle' }, + }, + { + id: 'v1', + config: { bundleSpec: 'http://localhost:3000/sample-vat.bundle' }, + }, + ]), + sendVatCommand: vi.fn((id: VatId, _message: KernelCommand) => { + if (id === 'v0') { + return 'success'; + } + return { error: 'Unknown vat ID' }; + }), + reset: vi.fn().mockResolvedValue(undefined), + } as unknown as Kernel; + + registry = new KernelCommandRegistry(); + handlers.forEach((handler) => { + registry.register(handler as CommandHandler); + }); + }); + + describe('vat management commands', () => { + it('should handle launchVat command', async () => { + const result = await registry.execute( + mockKernel, + mockKernelDatabase, + 'launchVat', + { + sourceSpec: 'bogus.js', + }, + ); + + expect(mockKernel.launchVat).toHaveBeenCalledWith({ + sourceSpec: 'bogus.js', + }); + expect(result).toBeNull(); + }); + + it('should handle invalid vat configuration', async () => { + setMockBehavior({ isVatConfig: false }); + + await expect( + registry.execute(mockKernel, mockKernelDatabase, 'launchVat', { + bogus: 'bogus.js', + } as unknown as VatConfig), + ).rejects.toThrow(/Expected a value of type `VatConfig`/u); + }); + + it('should handle restartVat command', async () => { + const result = await registry.execute( + mockKernel, + mockKernelDatabase, + 'restartVat', + { + id: 'v0', + }, + ); + + expect(mockKernel.restartVat).toHaveBeenCalledWith('v0'); + expect(result).toBeNull(); + }); + + it('should handle invalid vat ID for restartVat command', async () => { + setMockBehavior({ isVatId: false }); + + await expect( + registry.execute(mockKernel, mockKernelDatabase, 'restartVat', { + id: 'invalid', + }), + ).rejects.toThrow(/Expected a value of type `VatId`/u); + }); + + it('should handle terminateVat command', async () => { + const result = await registry.execute( + mockKernel, + mockKernelDatabase, + 'terminateVat', + { + id: 'v0', + }, + ); + + expect(mockKernel.terminateVat).toHaveBeenCalledWith('v0'); + expect(result).toBeNull(); + }); + + it('should handle terminateAllVats command', async () => { + const result = await registry.execute( + mockKernel, + mockKernelDatabase, + 'terminateAllVats', + [], + ); + + expect(mockKernel.terminateAllVats).toHaveBeenCalled(); + expect(result).toBeNull(); + }); + }); + + describe('status command', () => { + it('should handle getStatus command', async () => { + const result = await registry.execute( + mockKernel, + mockKernelDatabase, + 'getStatus', + [], + ); + + expect(mockKernel.getVats).toHaveBeenCalled(); + expect(result).toStrictEqual({ + clusterConfig: undefined, + vats: [ + { + id: 'v0', + config: { + bundleSpec: 'http://localhost:3000/sample-vat.bundle', + }, + }, + { + id: 'v1', + config: { + bundleSpec: 'http://localhost:3000/sample-vat.bundle', + }, + }, + ], + }); + }); + }); + + describe('sendVatCommand command', () => { + it('should handle vat commands', async () => { + const result = await registry.execute( + mockKernel, + mockKernelDatabase, + 'sendVatCommand', + { + id: 'v0', + payload: { method: 'ping', params: [] }, + }, + ); + + expect(mockKernel.sendVatCommand).toHaveBeenCalledWith('v0', { + method: 'ping', + params: [], + }); + expect(result).toStrictEqual({ result: 'success' }); + }); + + it('should handle invalid command payload', async () => { + setMockBehavior({ isKernelCommand: false }); + + await expect( + registry.execute(mockKernel, mockKernelDatabase, 'sendVatCommand', { + id: 'v0', + payload: { invalid: 'command' }, + }), + ).rejects.toThrow('Invalid command payload'); + }); + + it('should handle missing vat ID', async () => { + setMockBehavior({ isVatId: false }); + + await expect( + registry.execute(mockKernel, mockKernelDatabase, 'sendVatCommand', { + id: null, + payload: { method: 'ping', params: [] }, + }), + ).rejects.toThrow('Vat ID required for this command'); + }); + }); + + describe('error handling', () => { + it('should handle unknown method', async () => { + await expect( + // @ts-expect-error Testing invalid method + registry.execute(mockKernel, mockKernelDatabase, 'unknownMethod', null), + ).rejects.toThrow('Unknown method: unknownMethod'); + }); + + it('should handle kernel errors', async () => { + const error = new Error('Kernel error'); + vi.mocked(mockKernel.launchVat).mockRejectedValue(error); + + await expect( + registry.execute(mockKernel, mockKernelDatabase, 'launchVat', { + sourceSpec: 'bogus.js', + }), + ).rejects.toThrow('Kernel error'); + + vi.mocked(mockKernel.launchVat).mockRejectedValue('error'); + + await expect( + registry.execute(mockKernel, mockKernelDatabase, 'launchVat', { + sourceSpec: 'bogus.js', + }), + ).rejects.toThrow('error'); + }); + }); + + describe('clearState command', () => { + it('should handle clearState command', async () => { + const result = await registry.execute( + mockKernel, + mockKernelDatabase, + 'clearState', + [], + ); + + expect(mockKernel.reset).toHaveBeenCalled(); + expect(result).toBeNull(); + }); + + it('should handle clearState errors', async () => { + vi.mocked(mockKernel.reset).mockRejectedValue(new Error('Reset failed')); + + await expect( + registry.execute(mockKernel, mockKernelDatabase, 'clearState', []), + ).rejects.toThrow('Reset failed'); + }); + }); +}); diff --git a/packages/extension/src/kernel-integration/command-registry.ts b/packages/extension/src/kernel-integration/command-registry.ts index d89e00414..6a15a066e 100644 --- a/packages/extension/src/kernel-integration/command-registry.ts +++ b/packages/extension/src/kernel-integration/command-registry.ts @@ -36,18 +36,6 @@ export type CommandHandler = { ) => Promise; }; -export type Middleware = ( - next: ( - kernel: Kernel, - kernelDatabase: KernelDatabase, - params: unknown, - ) => Promise, -) => ( - kernel: Kernel, - kernelDatabase: KernelDatabase, - params: unknown, -) => Promise; - /** * A registry for kernel commands. */ @@ -57,8 +45,6 @@ export class KernelCommandRegistry { CommandHandler >(); - readonly #middlewares: Middleware[] = []; - /** * Register a command handler. * @@ -72,15 +58,6 @@ export class KernelCommandRegistry { this.#handlers.set(handler.method, handler); } - /** - * Register a middleware. - * - * @param middleware - The middleware. - */ - use(middleware: Middleware): void { - this.#middlewares.push(middleware); - } - /** * Execute a command. * @@ -101,20 +78,7 @@ export class KernelCommandRegistry { throw new Error(`Unknown method: ${method}`); } - let chain = async ( - k: Kernel, - kdb: KernelDatabase, - param: unknown, - ): Promise => { - assert(param, handler.schema); - return handler.implementation(k, kdb, param); - }; - - // Apply middlewares in reverse order - for (const middleware of [...this.#middlewares].reverse()) { - chain = middleware(chain); - } - - return chain(kernel, kernelDatabase, params); + assert(params, handler.schema); + return handler.implementation(kernel, kernelDatabase, params); } } diff --git a/packages/extension/src/kernel-integration/handle-panel-message.test.ts b/packages/extension/src/kernel-integration/handle-panel-message.test.ts deleted file mode 100644 index c0de6d4ac..000000000 --- a/packages/extension/src/kernel-integration/handle-panel-message.test.ts +++ /dev/null @@ -1,500 +0,0 @@ -import type { Kernel, KernelCommand, VatId, VatConfig } from '@ocap/kernel'; -import type { KernelDatabase } from '@ocap/store'; -import { setupOcapKernelMock } from '@ocap/test-utils'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; - -import type { KernelControlCommand } from './messages.ts'; - -// Mock logger -vi.mock('@ocap/utils', () => ({ - makeLogger: () => ({ - error: vi.fn(), - debug: vi.fn(), - }), -})); - -const { setMockBehavior, resetMocks } = setupOcapKernelMock(); - -describe('handlePanelMessage', () => { - let mockKernel: Kernel; - let mockKernelDatabase: KernelDatabase; - - beforeEach(() => { - vi.resetModules(); - resetMocks(); - - mockKernelDatabase = { - kernelKVStore: { - get: vi.fn(), - getRequired: vi.fn(), - getNextKey: vi.fn(), - set: vi.fn(), - delete: vi.fn(), - }, - clear: vi.fn(), - executeQuery: vi.fn(), - makeVatStore: vi.fn(), - }; - - // Create mock kernel - mockKernel = { - launchVat: vi.fn().mockResolvedValue(undefined), - restartVat: vi.fn().mockResolvedValue(undefined), - terminateVat: vi.fn().mockResolvedValue(undefined), - terminateAllVats: vi.fn().mockResolvedValue(undefined), - clearStorage: vi.fn().mockResolvedValue(undefined), - getVatIds: vi.fn().mockReturnValue(['v0', 'v1']), - getVats: vi.fn().mockReturnValue([ - { - id: 'v0', - config: { bundleSpec: 'http://localhost:3000/sample-vat.bundle' }, - }, - { - id: 'v1', - config: { bundleSpec: 'http://localhost:3000/sample-vat.bundle' }, - }, - ]), - sendVatCommand: vi.fn((id: VatId, _message: KernelCommand) => { - if (id === 'v0') { - return 'success'; - } - return { error: 'Unknown vat ID' }; - }), - reset: vi.fn().mockResolvedValue(undefined), - } as unknown as Kernel; - }); - - describe('vat management commands', () => { - it('should handle launchVat command', async () => { - const { handlePanelMessage } = await import('./handle-panel-message.ts'); - const message: KernelControlCommand = { - id: 'test-1', - payload: { - method: 'launchVat', - params: { sourceSpec: 'bogus.js' }, - }, - }; - - const response = await handlePanelMessage( - mockKernel, - mockKernelDatabase, - message, - ); - - expect(mockKernel.launchVat).toHaveBeenCalledWith({ - sourceSpec: 'bogus.js', - }); - expect(response).toStrictEqual({ - id: 'test-1', - payload: { - method: 'launchVat', - params: null, - }, - }); - }); - - it('should handle invalid vat configuration', async () => { - const { handlePanelMessage } = await import('./handle-panel-message.ts'); - setMockBehavior({ isVatConfig: false }); - - const message: KernelControlCommand = { - id: 'test-2', - payload: { - method: 'launchVat', - params: { bogus: 'bogus.js' } as unknown as VatConfig, - }, - }; - - const response = await handlePanelMessage( - mockKernel, - mockKernelDatabase, - message, - ); - - expect(response).toStrictEqual({ - id: 'test-2', - payload: { - method: 'launchVat', - params: { - error: - 'Expected a value of type `VatConfig`, but received: `[object Object]`', - }, - }, - }); - }); - - it('should handle restartVat command', async () => { - const { handlePanelMessage } = await import('./handle-panel-message.ts'); - const message: KernelControlCommand = { - id: 'test-3', - payload: { - method: 'restartVat', - params: { id: 'v0' }, - }, - }; - - const response = await handlePanelMessage( - mockKernel, - mockKernelDatabase, - message, - ); - - expect(mockKernel.restartVat).toHaveBeenCalledWith('v0'); - expect(response).toStrictEqual({ - id: 'test-3', - payload: { - method: 'restartVat', - params: null, - }, - }); - }); - - it('should handle invalid vat ID for restartVat command', async () => { - const { handlePanelMessage } = await import('./handle-panel-message.ts'); - setMockBehavior({ isVatId: false }); - - const message: KernelControlCommand = { - id: 'test-4', - payload: { - method: 'restartVat', - params: { id: 'invalid' }, - }, - }; - - const response = await handlePanelMessage( - mockKernel, - mockKernelDatabase, - message, - ); - - expect(response).toStrictEqual({ - id: 'test-4', - payload: { - method: 'restartVat', - params: { - error: - 'At path: id -- Expected a value of type `VatId`, but received: `"invalid"`', - }, - }, - }); - }); - - it('should handle terminateVat command', async () => { - const { handlePanelMessage } = await import('./handle-panel-message.ts'); - const message: KernelControlCommand = { - id: 'test-5', - payload: { - method: 'terminateVat', - params: { id: 'v0' }, - }, - }; - - const response = await handlePanelMessage( - mockKernel, - mockKernelDatabase, - message, - ); - - expect(mockKernel.terminateVat).toHaveBeenCalledWith('v0'); - expect(response).toStrictEqual({ - id: 'test-5', - payload: { - method: 'terminateVat', - params: null, - }, - }); - }); - - it('should handle terminateAllVats command', async () => { - const { handlePanelMessage } = await import('./handle-panel-message.ts'); - const message: KernelControlCommand = { - id: 'test-6', - payload: { - method: 'terminateAllVats', - params: null, - }, - }; - - const response = await handlePanelMessage( - mockKernel, - mockKernelDatabase, - message, - ); - - expect(mockKernel.terminateAllVats).toHaveBeenCalled(); - expect(response).toStrictEqual({ - id: 'test-6', - payload: { - method: 'terminateAllVats', - params: null, - }, - }); - }); - }); - - describe('status command', () => { - it('should handle getStatus command', async () => { - const { handlePanelMessage } = await import('./handle-panel-message.ts'); - const message: KernelControlCommand = { - id: 'test-7', - payload: { - method: 'getStatus', - params: null, - }, - }; - - const response = await handlePanelMessage( - mockKernel, - mockKernelDatabase, - message, - ); - - expect(mockKernel.getVats).toHaveBeenCalled(); - expect(response).toStrictEqual({ - id: 'test-7', - payload: { - method: 'getStatus', - params: { - clusterConfig: undefined, - vats: [ - { - id: 'v0', - config: { - bundleSpec: 'http://localhost:3000/sample-vat.bundle', - }, - }, - { - id: 'v1', - config: { - bundleSpec: 'http://localhost:3000/sample-vat.bundle', - }, - }, - ], - }, - }, - }); - }); - }); - - describe('sendVatCommand command', () => { - it('should handle vat commands', async () => { - const { handlePanelMessage } = await import('./handle-panel-message.ts'); - const message: KernelControlCommand = { - id: 'test-11', - payload: { - method: 'sendVatCommand', - params: { - id: 'v0', - payload: { method: 'ping', params: null }, - }, - }, - }; - - const response = await handlePanelMessage( - mockKernel, - mockKernelDatabase, - message, - ); - - expect(mockKernel.sendVatCommand).toHaveBeenCalledWith('v0', { - method: 'ping', - params: null, - }); - expect(response).toStrictEqual({ - id: 'test-11', - payload: { - method: 'sendVatCommand', - params: { result: 'success' }, - }, - }); - }); - - it('should handle invalid command payload', async () => { - const { handlePanelMessage } = await import('./handle-panel-message.ts'); - const kernel = await import('@ocap/kernel'); - const kernelSpy = vi.spyOn(kernel, 'isKernelCommand'); - kernelSpy.mockReturnValue(false); - - const message: KernelControlCommand = { - id: 'test-12', - payload: { - method: 'sendVatCommand', - params: { - payload: { invalid: 'command' }, - }, - }, - }; - - const response = await handlePanelMessage( - mockKernel, - mockKernelDatabase, - message, - ); - - expect(response).toStrictEqual({ - id: 'test-12', - payload: { - method: 'sendVatCommand', - params: { error: 'Invalid command payload' }, - }, - }); - }); - - it('should handle missing vat ID', async () => { - const { handlePanelMessage } = await import('./handle-panel-message.ts'); - const kernel = await import('@ocap/kernel'); - const isVatIdSpy = vi.spyOn(kernel, 'isVatId'); - isVatIdSpy.mockReturnValue(false); - - const message: KernelControlCommand = { - id: 'test-13', - payload: { - method: 'sendVatCommand', - params: { - payload: { method: 'ping', params: null }, - }, - }, - }; - - const response = await handlePanelMessage( - mockKernel, - mockKernelDatabase, - message, - ); - - expect(response).toStrictEqual({ - id: 'test-13', - payload: { - method: 'sendVatCommand', - params: { error: 'Vat ID required for this command' }, - }, - }); - }); - }); - - describe('error handling', () => { - it('should handle unknown method', async () => { - const { handlePanelMessage } = await import('./handle-panel-message.ts'); - const message: KernelControlCommand = { - id: 'test-14', - payload: { - method: 'unknownMethod', - params: null, - }, - } as unknown as KernelControlCommand; - - const response = await handlePanelMessage( - mockKernel, - mockKernelDatabase, - message, - ); - - expect(response).toStrictEqual({ - id: 'test-14', - payload: { - method: 'unknownMethod', - params: { error: 'Unknown method: unknownMethod' }, - }, - }); - }); - - it('should handle kernel errors', async () => { - const { handlePanelMessage } = await import('./handle-panel-message.ts'); - const error = new Error('Kernel error'); - vi.mocked(mockKernel.launchVat).mockRejectedValue(error); - - const message: KernelControlCommand = { - id: 'test-15', - payload: { - method: 'launchVat', - params: { sourceSpec: 'bogus.js' }, - }, - }; - - const response = await handlePanelMessage( - mockKernel, - mockKernelDatabase, - message, - ); - - expect(response).toStrictEqual({ - id: 'test-15', - payload: { - method: 'launchVat', - params: { error: 'Kernel error' }, - }, - }); - - vi.mocked(mockKernel.launchVat).mockRejectedValue('error'); - - const response2 = await handlePanelMessage( - mockKernel, - mockKernelDatabase, - message, - ); - - expect(response2).toStrictEqual({ - id: 'test-15', - payload: { - method: 'launchVat', - params: { error: 'error' }, - }, - }); - }); - }); - - describe('clearState command', () => { - it('should handle clearState command', async () => { - const { handlePanelMessage } = await import('./handle-panel-message.ts'); - const message: KernelControlCommand = { - id: 'test-16', - payload: { - method: 'clearState', - params: null, - }, - }; - - const response = await handlePanelMessage( - mockKernel, - mockKernelDatabase, - message, - ); - - expect(mockKernel.reset).toHaveBeenCalled(); - expect(response).toStrictEqual({ - id: 'test-16', - payload: { - method: 'clearState', - params: null, - }, - }); - }); - - it('should handle clearState errors', async () => { - const { handlePanelMessage } = await import('./handle-panel-message.ts'); - vi.mocked(mockKernel.reset).mockRejectedValue(new Error('Reset failed')); - - const message: KernelControlCommand = { - id: 'test-17', - payload: { - method: 'clearState', - params: null, - }, - }; - - const response = await handlePanelMessage( - mockKernel, - mockKernelDatabase, - message, - ); - - expect(response).toStrictEqual({ - id: 'test-17', - payload: { - method: 'clearState', - params: { error: 'Reset failed' }, - }, - }); - }); - }); -}); diff --git a/packages/extension/src/kernel-integration/handle-panel-message.ts b/packages/extension/src/kernel-integration/handle-panel-message.ts deleted file mode 100644 index e7a74f033..000000000 --- a/packages/extension/src/kernel-integration/handle-panel-message.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { Kernel } from '@ocap/kernel'; -import type { KernelDatabase } from '@ocap/store'; -import { makeLogger } from '@ocap/utils'; - -import { KernelCommandRegistry } from './command-registry.ts'; -import type { CommandHandler } from './command-registry.ts'; -import { handlers } from './handlers/index.ts'; -import type { KernelControlCommand, KernelControlReply } from './messages.ts'; -import { loggingMiddleware } from './middlewares/logging.ts'; - -const logger = makeLogger('[kernel-panel]'); -const registry = new KernelCommandRegistry(); - -// Register middlewares -registry.use(loggingMiddleware); - -// Register handlers -handlers.forEach((handler) => - registry.register(handler as CommandHandler), -); - -/** - * Handles a message from the panel. - * - * @param kernel - The kernel instance. - * @param kernelDatabase - The kernel database instance. - * @param message - The message to handle. - * @returns The reply to the message. - */ -export async function handlePanelMessage( - kernel: Kernel, - kernelDatabase: KernelDatabase, - message: KernelControlCommand, -): Promise { - const { method, params } = message.payload; - - try { - const result = await registry.execute( - kernel, - kernelDatabase, - method, - params, - ); - - return { - id: message.id, - payload: { - method, - params: result, - }, - } as KernelControlReply; - } catch (error) { - logger.error('Error handling message:', error); - return { - id: message.id, - payload: { - method, - params: { - error: error instanceof Error ? error.message : String(error), - }, - }, - } as KernelControlReply; - } -} diff --git a/packages/extension/src/kernel-integration/handlers/clear-state.test.ts b/packages/extension/src/kernel-integration/handlers/clear-state.test.ts index 13d683b3c..6d480b14f 100644 --- a/packages/extension/src/kernel-integration/handlers/clear-state.test.ts +++ b/packages/extension/src/kernel-integration/handlers/clear-state.test.ts @@ -23,7 +23,7 @@ describe('clearStateHandler', () => { const result = await clearStateHandler.implementation( mockKernel, mockKernelDatabase, - null, + [], ); expect(mockKernel.reset).toHaveBeenCalledOnce(); expect(result).toBeNull(); @@ -33,7 +33,7 @@ describe('clearStateHandler', () => { const error = new Error('Reset failed'); vi.mocked(mockKernel.reset).mockRejectedValueOnce(error); await expect( - clearStateHandler.implementation(mockKernel, mockKernelDatabase, null), + clearStateHandler.implementation(mockKernel, mockKernelDatabase, []), ).rejects.toThrow(error); }); }); diff --git a/packages/extension/src/kernel-integration/handlers/get-status.test.ts b/packages/extension/src/kernel-integration/handlers/get-status.test.ts index 5f0a5b2ad..f6f15cdad 100644 --- a/packages/extension/src/kernel-integration/handlers/get-status.test.ts +++ b/packages/extension/src/kernel-integration/handlers/get-status.test.ts @@ -30,7 +30,7 @@ describe('getStatusHandler', () => { const result = await getStatusHandler.implementation( mockKernel, mockKernelDatabase, - null, + [], ); expect(mockKernel.getVats).toHaveBeenCalledOnce(); expect(result).toStrictEqual({ vats: mockVats, clusterConfig }); diff --git a/packages/extension/src/kernel-integration/handlers/reload-config.test.ts b/packages/extension/src/kernel-integration/handlers/reload-config.test.ts index 622668dde..cf918c89a 100644 --- a/packages/extension/src/kernel-integration/handlers/reload-config.test.ts +++ b/packages/extension/src/kernel-integration/handlers/reload-config.test.ts @@ -19,7 +19,7 @@ describe('reloadConfigHandler', () => { const result = await reloadConfigHandler.implementation( mockKernel as Kernel, mockKernelDatabase, - null, + [], ); expect(mockKernel.reload).toHaveBeenCalledTimes(1); @@ -42,7 +42,7 @@ describe('reloadConfigHandler', () => { reloadConfigHandler.implementation( mockKernel as Kernel, mockKernelDatabase, - null, + [], ), ).rejects.toThrow(error); }); diff --git a/packages/extension/src/kernel-integration/handlers/send-vat-command.test.ts b/packages/extension/src/kernel-integration/handlers/send-vat-command.test.ts index 2df5049ab..fe8b4b2b3 100644 --- a/packages/extension/src/kernel-integration/handlers/send-vat-command.test.ts +++ b/packages/extension/src/kernel-integration/handlers/send-vat-command.test.ts @@ -18,8 +18,8 @@ describe('sendVatCommandHandler', () => { it('should handle vat messages', async () => { const params = { id: 'v0', - payload: { method: 'ping', params: null }, - } as const; + payload: { method: 'ping', params: [] }, + }; const result = await sendVatCommandHandler.implementation( mockKernel, mockKernelDatabase, @@ -27,21 +27,17 @@ describe('sendVatCommandHandler', () => { ); expect(mockKernel.sendVatCommand).toHaveBeenCalledWith('v0', { method: 'ping', - params: null, + params: [], }); expect(result).toStrictEqual({ result: 'success' }); }); it('should throw error when vat ID is missing', async () => { - const params = { - payload: { method: 'ping', params: null }, - }; await expect( - sendVatCommandHandler.implementation( - mockKernel, - mockKernelDatabase, - params, - ), + sendVatCommandHandler.implementation(mockKernel, mockKernelDatabase, { + id: null, + payload: { method: 'ping', params: [] }, + }), ).rejects.toThrow('Vat ID required for this command'); }); }); diff --git a/packages/extension/src/kernel-integration/handlers/terminate-all-vats.test.ts b/packages/extension/src/kernel-integration/handlers/terminate-all-vats.test.ts index 8c54b1b0a..80107a686 100644 --- a/packages/extension/src/kernel-integration/handlers/terminate-all-vats.test.ts +++ b/packages/extension/src/kernel-integration/handlers/terminate-all-vats.test.ts @@ -19,7 +19,7 @@ describe('terminateAllVatsHandler', () => { const result = await terminateAllVatsHandler.implementation( mockKernel, mockKernelDatabase, - null, + [], ); expect(mockKernel.terminateAllVats).toHaveBeenCalledOnce(); expect(result).toBeNull(); @@ -32,7 +32,7 @@ describe('terminateAllVatsHandler', () => { terminateAllVatsHandler.implementation( mockKernel, mockKernelDatabase, - null, + [], ), ).rejects.toThrow(error); }); diff --git a/packages/extension/src/kernel-integration/handlers/update-cluster-config.test.ts b/packages/extension/src/kernel-integration/handlers/update-cluster-config.test.ts index bc0916c08..1048aaadb 100644 --- a/packages/extension/src/kernel-integration/handlers/update-cluster-config.test.ts +++ b/packages/extension/src/kernel-integration/handlers/update-cluster-config.test.ts @@ -1,4 +1,4 @@ -import type { Kernel } from '@ocap/kernel'; +import type { ClusterConfig, Kernel } from '@ocap/kernel'; import type { KernelDatabase } from '@ocap/store'; import { describe, it, expect } from 'vitest'; @@ -11,8 +11,10 @@ describe('updateClusterConfigHandler', () => { const mockKernelDatabase = {} as KernelDatabase; - const testConfig = { + const testConfig: ClusterConfig = { bootstrap: 'testVat', + forceReset: true, + bundles: null, vats: { testVat: { sourceSpec: 'test-source', diff --git a/packages/extension/src/kernel-integration/kernel-worker.ts b/packages/extension/src/kernel-integration/kernel-worker.ts index 3268e710d..d5451b204 100644 --- a/packages/extension/src/kernel-integration/kernel-worker.ts +++ b/packages/extension/src/kernel-integration/kernel-worker.ts @@ -1,3 +1,4 @@ +import { JsonRpcEngine } from '@metamask/json-rpc-engine'; import type { ClusterConfig, KernelCommand, @@ -12,7 +13,8 @@ import { } from '@ocap/streams/browser'; import { fetchValidatedJson, makeLogger } from '@ocap/utils'; -import { handlePanelMessage } from './handle-panel-message.ts'; +import { loggingMiddleware } from './middleware/logging.ts'; +import { createPanelMessageMiddleware } from './middleware/panel-message.ts'; import { receiveUiConnections } from './ui-connections.ts'; import { ExtensionVatWorkerClient } from './VatWorkerClient.ts'; @@ -55,10 +57,10 @@ async function main(): Promise { resetStorage: true, }, ); - receiveUiConnections( - async (message) => handlePanelMessage(kernel, kernelDatabase, message), - logger, - ); + const kernelEngine = new JsonRpcEngine(); + kernelEngine.push(loggingMiddleware); + kernelEngine.push(createPanelMessageMiddleware(kernel, kernelDatabase)); + receiveUiConnections(async (request) => kernelEngine.handle(request), logger); const defaultSubcluster = await fetchValidatedJson( new URL('../vats/default-cluster.json', import.meta.url).href, diff --git a/packages/extension/src/kernel-integration/messages.test.ts b/packages/extension/src/kernel-integration/messages.test.ts index 078279866..4370ed2a8 100644 --- a/packages/extension/src/kernel-integration/messages.test.ts +++ b/packages/extension/src/kernel-integration/messages.test.ts @@ -12,68 +12,50 @@ describe('isKernelControlCommand', () => { [ 'launch vat command', { - id: 'test-1', - payload: { - method: 'launchVat', - params: { sourceSpec: 'test.js' }, - }, + method: 'launchVat', + params: { sourceSpec: 'test.js' }, }, true, ], [ 'restart vat command', { - id: 'test-1', - payload: { - method: 'restartVat', - params: { id: 'v0' }, - }, + method: 'restartVat', + params: { id: 'v0' }, }, true, ], [ 'terminate vat command', { - id: 'test-1', - payload: { - method: 'terminateVat', - params: { id: 'v0' }, - }, + method: 'terminateVat', + params: { id: 'v0' }, }, true, ], [ 'terminate all vats command', { - id: 'test-1', - payload: { - method: 'terminateAllVats', - params: null, - }, + method: 'terminateAllVats', + params: [], }, true, ], [ 'get status command', { - id: 'test-1', - payload: { - method: 'getStatus', - params: null, - }, + method: 'getStatus', + params: [], }, true, ], [ 'send message command', { - id: 'test-1', - payload: { - method: 'sendVatCommand', - params: { - id: 'v0', - payload: { test: 'data' }, - }, + method: 'sendVatCommand', + params: { + id: 'v0', + payload: { test: 'data' }, }, }, true, @@ -81,23 +63,17 @@ describe('isKernelControlCommand', () => { [ 'clear state command', { - id: 'test-1', - payload: { - method: 'clearState', - params: null, - }, + method: 'clearState', + params: [], }, true, ], [ 'execute DB query command', { - id: 'test-1', - payload: { - method: 'executeDBQuery', - params: { - sql: 'SELECT * FROM test', - }, + method: 'executeDBQuery', + params: { + sql: 'SELECT * FROM test', }, }, true, @@ -136,40 +112,31 @@ describe('isKernelControlReply', () => { [ 'launch vat success reply', { - id: 'test-1', - payload: { - method: 'launchVat', - params: null, - }, + method: 'launchVat', + result: null, }, true, ], [ 'launch vat error reply', { - id: 'test-1', - payload: { - method: 'launchVat', - params: { error: 'Failed to launch vat' }, - }, + method: 'launchVat', + result: { error: 'Failed to launch vat' }, }, true, ], [ 'get status reply', { - id: 'test-1', - payload: { - method: 'getStatus', - params: { - clusterConfig, - vats: [ - { - id: 'v0', - config: { sourceSpec: 'test.js' }, - }, - ], - }, + method: 'getStatus', + result: { + clusterConfig, + vats: [ + { + id: 'v0', + config: { sourceSpec: 'test.js' }, + }, + ], }, }, true, @@ -177,11 +144,8 @@ describe('isKernelControlReply', () => { [ 'send message reply', { - id: 'test-1', - payload: { - method: 'sendVatCommand', - params: { result: 'success' }, - }, + method: 'sendVatCommand', + result: { result: 'success' }, }, true, ], @@ -193,19 +157,16 @@ describe('isKernelControlReply', () => { [ 'invalid method', { - id: 'test', - payload: { method: 'invalidMethod' }, + method: 'invalidMethod', + result: null, }, false, ], [ - 'invalid params', + 'invalid result', { - id: 'test', - payload: { - method: 'launchVat', - params: 'invalid', - }, + method: 'launchVat', + result: 'invalid', }, false, ], diff --git a/packages/extension/src/kernel-integration/messages.ts b/packages/extension/src/kernel-integration/messages.ts index 8fcceaf74..96ee42425 100644 --- a/packages/extension/src/kernel-integration/messages.ts +++ b/packages/extension/src/kernel-integration/messages.ts @@ -9,13 +9,13 @@ import { record, } from '@metamask/superstruct'; import type { Infer } from '@metamask/superstruct'; -import type { Json } from '@metamask/utils'; import { UnsafeJsonStruct } from '@metamask/utils'; import { ClusterConfigStruct, VatConfigStruct, VatIdStruct, } from '@ocap/kernel'; +import { EmptyJsonArray } from '@ocap/utils'; import type { TypeGuard } from '@ocap/utils'; const KernelStatusStruct = type({ @@ -46,26 +46,26 @@ export const KernelCommandPayloadStructs = { }), terminateAllVats: object({ method: literal('terminateAllVats'), - params: literal(null), + params: EmptyJsonArray, }), getStatus: object({ method: literal('getStatus'), - params: literal(null), + params: EmptyJsonArray, }), reload: object({ method: literal('reload'), - params: literal(null), + params: EmptyJsonArray, }), sendVatCommand: object({ method: literal('sendVatCommand'), params: object({ - id: union([VatIdStruct, literal(undefined)]), + id: union([VatIdStruct, literal(null)]), payload: UnsafeJsonStruct, }), }), clearState: object({ method: literal('clearState'), - params: literal(null), + params: EmptyJsonArray, }), executeDBQuery: object({ method: literal('executeDBQuery'), @@ -84,91 +84,84 @@ export const KernelCommandPayloadStructs = { export const KernelReplyPayloadStructs = { launchVat: object({ method: literal('launchVat'), - params: union([literal(null), object({ error: string() })]), + result: union([literal(null), object({ error: string() })]), }), restartVat: object({ method: literal('restartVat'), - params: union([literal(null), object({ error: string() })]), + result: union([literal(null), object({ error: string() })]), }), terminateVat: object({ method: literal('terminateVat'), - params: union([literal(null), object({ error: string() })]), + result: union([literal(null), object({ error: string() })]), }), terminateAllVats: object({ method: literal('terminateAllVats'), - params: union([literal(null), object({ error: string() })]), + result: union([literal(null), object({ error: string() })]), }), getStatus: object({ method: literal('getStatus'), - params: union([KernelStatusStruct, object({ error: string() })]), + result: union([KernelStatusStruct, object({ error: string() })]), }), reload: object({ method: literal('reload'), - params: union([literal(null), object({ error: string() })]), + result: union([literal(null), object({ error: string() })]), }), sendVatCommand: object({ method: literal('sendVatCommand'), - params: UnsafeJsonStruct, + result: UnsafeJsonStruct, }), clearState: object({ method: literal('clearState'), - params: literal(null), + result: literal(null), }), executeDBQuery: object({ method: literal('executeDBQuery'), - params: union([ + result: union([ array(record(string(), string())), object({ error: string() }), ]), }), updateClusterConfig: object({ method: literal('updateClusterConfig'), - params: literal(null), + result: literal(null), }), } as const; -const KernelControlCommandStruct = object({ - id: string(), - payload: union([ - KernelCommandPayloadStructs.launchVat, - KernelCommandPayloadStructs.restartVat, - KernelCommandPayloadStructs.terminateVat, - KernelCommandPayloadStructs.terminateAllVats, - KernelCommandPayloadStructs.getStatus, - KernelCommandPayloadStructs.reload, - KernelCommandPayloadStructs.sendVatCommand, - KernelCommandPayloadStructs.clearState, - KernelCommandPayloadStructs.executeDBQuery, - KernelCommandPayloadStructs.updateClusterConfig, - ]), -}); - -const KernelControlReplyStruct = object({ - id: string(), - payload: union([ - KernelReplyPayloadStructs.launchVat, - KernelReplyPayloadStructs.restartVat, - KernelReplyPayloadStructs.terminateVat, - KernelReplyPayloadStructs.terminateAllVats, - KernelReplyPayloadStructs.getStatus, - KernelReplyPayloadStructs.reload, - KernelReplyPayloadStructs.sendVatCommand, - KernelReplyPayloadStructs.clearState, - KernelReplyPayloadStructs.executeDBQuery, - KernelReplyPayloadStructs.updateClusterConfig, - ]), -}); - -export type KernelControlCommand = Infer & - Json; +const KernelControlCommandStruct = union([ + KernelCommandPayloadStructs.launchVat, + KernelCommandPayloadStructs.restartVat, + KernelCommandPayloadStructs.terminateVat, + KernelCommandPayloadStructs.terminateAllVats, + KernelCommandPayloadStructs.getStatus, + KernelCommandPayloadStructs.reload, + KernelCommandPayloadStructs.sendVatCommand, + KernelCommandPayloadStructs.clearState, + KernelCommandPayloadStructs.executeDBQuery, + KernelCommandPayloadStructs.updateClusterConfig, +]); + +const KernelControlReplyStruct = union([ + KernelReplyPayloadStructs.launchVat, + KernelReplyPayloadStructs.restartVat, + KernelReplyPayloadStructs.terminateVat, + KernelReplyPayloadStructs.terminateAllVats, + KernelReplyPayloadStructs.getStatus, + KernelReplyPayloadStructs.reload, + KernelReplyPayloadStructs.sendVatCommand, + KernelReplyPayloadStructs.clearState, + KernelReplyPayloadStructs.executeDBQuery, + KernelReplyPayloadStructs.updateClusterConfig, +]); + +export type KernelControlCommand = Infer; export type KernelControlReply = Infer; -export type KernelReplyParams< +export type KernelControlResult< Method extends keyof typeof KernelReplyPayloadStructs, -> = Infer<(typeof KernelReplyPayloadStructs)[Method]>['params']; +> = Infer<(typeof KernelReplyPayloadStructs)[Method]>['result']; export type KernelControlReturnType = { - [Method in keyof typeof KernelReplyPayloadStructs]: KernelReplyParams; + [Method in keyof typeof KernelReplyPayloadStructs]: KernelControlResult; }; export const isKernelControlCommand: TypeGuard = ( diff --git a/packages/extension/src/kernel-integration/middleware/logging.test.ts b/packages/extension/src/kernel-integration/middleware/logging.test.ts new file mode 100644 index 000000000..22adc0c9e --- /dev/null +++ b/packages/extension/src/kernel-integration/middleware/logging.test.ts @@ -0,0 +1,107 @@ +import { + createAsyncMiddleware, + JsonRpcEngine, +} from '@metamask/json-rpc-engine'; +import type { JsonRpcRequest, JsonRpcSuccess } from '@metamask/utils'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import { loggingMiddleware, logger } from './logging.ts'; + +describe('loggingMiddleware', () => { + let engine: JsonRpcEngine; + + beforeEach(() => { + vi.clearAllMocks(); + engine = new JsonRpcEngine(); + engine.push(loggingMiddleware); + }); + + it('should pass the request to the next middleware', async () => { + // Create a spy middleware to verify the request is passed through + const nextSpy = vi.fn((_req, res, next) => { + res.result = 'success'; + return next(); + }); + engine.push(nextSpy); + + const request: JsonRpcRequest = { + id: 1, + jsonrpc: '2.0', + method: 'test', + params: { foo: 'bar' }, + }; + + await engine.handle(request); + expect(nextSpy).toHaveBeenCalled(); + }); + + it('should return the result from the next middleware', async () => { + // Add a middleware that sets a result + engine.push((_req, res, _next, end) => { + res.result = 'test result'; + return end(); + }); + + const request: JsonRpcRequest = { + id: 2, + jsonrpc: '2.0', + method: 'test', + params: {}, + }; + + const response = (await engine.handle(request)) as JsonRpcSuccess; + expect(response.result).toBe('test result'); + }); + + it('should log the execution duration', async () => { + const debugSpy = vi.spyOn(logger, 'debug'); + + // Add a middleware that introduces a delay + engine.push( + createAsyncMiddleware(async (_req, res, _next) => { + await new Promise((resolve) => setTimeout(resolve, 10)); + res.result = 'delayed result'; + }), + ); + + const request: JsonRpcRequest = { + id: 3, + jsonrpc: '2.0', + method: 'test', + params: {}, + }; + + await engine.handle(request); + + expect(debugSpy).toHaveBeenCalledWith( + expect.stringMatching(/Command executed in \d*\.?\d+ms/u), + ); + }); + + it('should log duration even if next middleware throws', async () => { + const debugSpy = vi.spyOn(logger, 'debug'); + const error = new Error('Test error'); + + // Add a middleware that throws an error + engine.push(() => { + throw error; + }); + + const request: JsonRpcRequest = { + id: 4, + jsonrpc: '2.0', + method: 'test', + params: {}, + }; + + expect(await engine.handle(request)).toMatchObject({ + error: expect.objectContaining({ + message: 'Test error', + }), + }); + + expect(debugSpy).toHaveBeenCalledWith( + expect.stringMatching(/Command executed in \d*\.?\d+ms/u), + ); + }); +}); diff --git a/packages/extension/src/kernel-integration/middleware/logging.ts b/packages/extension/src/kernel-integration/middleware/logging.ts new file mode 100644 index 000000000..abbe9a051 --- /dev/null +++ b/packages/extension/src/kernel-integration/middleware/logging.ts @@ -0,0 +1,15 @@ +import { createAsyncMiddleware } from '@metamask/json-rpc-engine'; +import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; +import type { Json, JsonRpcParams } from '@metamask/utils'; +import { makeLogger } from '@ocap/utils'; + +export const logger = makeLogger('[kernel-commands]'); + +export const loggingMiddleware: JsonRpcMiddleware = + createAsyncMiddleware(async (_req, _res, next) => { + const start = performance.now(); + // eslint-disable-next-line n/callback-return + await next(); + const duration = performance.now() - start; + logger.debug(`Command executed in ${duration}ms`); + }); diff --git a/packages/extension/src/kernel-integration/middleware/panel-message.test.ts b/packages/extension/src/kernel-integration/middleware/panel-message.test.ts new file mode 100644 index 000000000..a6fb85d8d --- /dev/null +++ b/packages/extension/src/kernel-integration/middleware/panel-message.test.ts @@ -0,0 +1,204 @@ +import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import type { JsonRpcRequest } from '@metamask/utils'; +import type { Kernel } from '@ocap/kernel'; +import type { KernelDatabase } from '@ocap/store'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import { createPanelMessageMiddleware } from './panel-message.ts'; + +const { mockRegister, mockExecute } = vi.hoisted(() => ({ + mockRegister: vi.fn(), + mockExecute: vi.fn(), +})); + +vi.mock('../command-registry.ts', () => ({ + KernelCommandRegistry: vi.fn().mockImplementation(() => ({ + register: mockRegister, + execute: mockExecute, + })), +})); + +// Mock the handlers +vi.mock('../handlers/index.ts', () => ({ + handlers: [{ method: 'testMethod1' }, { method: 'testMethod2' }], +})); + +describe('createPanelMessageMiddleware', () => { + let mockKernel: Kernel; + let mockKernelDatabase: KernelDatabase; + let engine: JsonRpcEngine; + + beforeEach(() => { + // Set up mocks + mockKernel = {} as Kernel; + mockKernelDatabase = {} as KernelDatabase; + + // Create a new JSON-RPC engine with our middleware + engine = new JsonRpcEngine(); + engine.push(createPanelMessageMiddleware(mockKernel, mockKernelDatabase)); + }); + + it('should handle successful command execution', async () => { + // Set up the mock to return a successful result + const expectedResult = { success: true, data: 'test data' }; + mockExecute.mockResolvedValueOnce(expectedResult); + + // Create a request + const request = { + id: 1, + jsonrpc: '2.0', + method: 'testMethod1', + params: { foo: 'bar' }, + } as JsonRpcRequest; + + // Process the request + const response = await engine.handle(request); + + // Verify the middleware called execute with the right parameters + expect(mockExecute).toHaveBeenCalledWith( + mockKernel, + mockKernelDatabase, + 'testMethod1', + { foo: 'bar' }, + ); + + // Verify the response contains the expected result + expect(response).toStrictEqual({ + id: 1, + jsonrpc: '2.0', + result: expectedResult, + }); + }); + + it('should handle command execution with empty params', async () => { + // Set up the mock to return a successful result + mockExecute.mockResolvedValueOnce(null); + + // Create a request with no params + const request = { + id: 2, + jsonrpc: '2.0', + method: 'testMethod2', + params: [], + } as JsonRpcRequest; + + // Process the request + const response = await engine.handle(request); + + // Verify the middleware called execute with the right parameters + expect(mockExecute).toHaveBeenCalledWith( + mockKernel, + mockKernelDatabase, + 'testMethod2', + [], + ); + + // Verify the response contains the expected result + expect(response).toStrictEqual({ + id: 2, + jsonrpc: '2.0', + result: null, + }); + }); + + it('should handle command execution errors', async () => { + // Set up the mock to throw an error + const error = new Error('Test error'); + mockExecute.mockRejectedValueOnce(error); + + // Create a request + const request = { + id: 3, + jsonrpc: '2.0', + method: 'testMethod1', + params: { foo: 'bar' }, + } as JsonRpcRequest; + + // Process the request + const response = await engine.handle(request); + + // Verify the middleware called execute + expect(mockExecute).toHaveBeenCalledWith( + mockKernel, + mockKernelDatabase, + 'testMethod1', + { foo: 'bar' }, + ); + + // Verify the response contains the error + expect(response).toStrictEqual({ + id: 3, + jsonrpc: '2.0', + error: expect.objectContaining({ + code: -32603, // Internal error + data: expect.objectContaining({ + cause: expect.objectContaining({ + message: 'Test error', + }), + }), + }), + }); + }); + + it('should handle array params', async () => { + // Set up the mock to return a successful result + mockExecute.mockResolvedValueOnce({ result: 'array processed' }); + + // Create a request with array params + const request = { + id: 4, + jsonrpc: '2.0', + method: 'testMethod1', + params: ['item1', 'item2'], + } as JsonRpcRequest; + + // Process the request + const response = await engine.handle(request); + + // Verify the middleware called execute with the array params + expect(mockExecute).toHaveBeenCalledWith( + mockKernel, + mockKernelDatabase, + 'testMethod1', + ['item1', 'item2'], + ); + + // Verify the response contains the expected result + expect(response).toStrictEqual({ + id: 4, + jsonrpc: '2.0', + result: { result: 'array processed' }, + }); + }); + + it('should handle requests without params', async () => { + // Set up the mock to return a successful result + mockExecute.mockResolvedValueOnce({ status: 'ok' }); + + // Create a request without params + const request = { + id: 5, + jsonrpc: '2.0', + method: 'testMethod2', + // No params field + } as JsonRpcRequest; + + // Process the request + const response = await engine.handle(request); + + // Verify the middleware called execute with undefined params + expect(mockExecute).toHaveBeenCalledWith( + mockKernel, + mockKernelDatabase, + 'testMethod2', + undefined, + ); + + // Verify the response contains the expected result + expect(response).toStrictEqual({ + id: 5, + jsonrpc: '2.0', + result: { status: 'ok' }, + }); + }); +}); diff --git a/packages/extension/src/kernel-integration/middleware/panel-message.ts b/packages/extension/src/kernel-integration/middleware/panel-message.ts new file mode 100644 index 000000000..21e277195 --- /dev/null +++ b/packages/extension/src/kernel-integration/middleware/panel-message.ts @@ -0,0 +1,36 @@ +import { createAsyncMiddleware } from '@metamask/json-rpc-engine'; +import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; +import type { Json } from '@metamask/utils'; +import type { Kernel } from '@ocap/kernel'; +import type { KernelDatabase } from '@ocap/store'; + +import { KernelCommandRegistry } from '../command-registry.ts'; +import type { CommandHandler } from '../command-registry.ts'; +import { handlers } from '../handlers/index.ts'; +import type { KernelControlCommand } from '../messages.ts'; + +const registry = new KernelCommandRegistry(); + +// Register handlers +handlers.forEach((handler) => + registry.register(handler as CommandHandler), +); + +type KernelControlParams = KernelControlCommand['params']; + +/** + * Creates a middleware function that handles panel messages. + * + * @param kernel - The kernel instance. + * @param kernelDatabase - The kernel database instance. + * @returns The middleware function. + */ +export const createPanelMessageMiddleware = ( + kernel: Kernel, + kernelDatabase: KernelDatabase, +): JsonRpcMiddleware => + createAsyncMiddleware(async (req, res, _next) => { + const { method, params } = req; + // @ts-expect-error - TODO:rekm execute() should probably just expect a string "method" + res.result = await registry.execute(kernel, kernelDatabase, method, params); + }); diff --git a/packages/extension/src/kernel-integration/middlewares/logging.test.ts b/packages/extension/src/kernel-integration/middlewares/logging.test.ts deleted file mode 100644 index 8ef65ade0..000000000 --- a/packages/extension/src/kernel-integration/middlewares/logging.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { Kernel } from '@ocap/kernel'; -import type { KernelDatabase } from '@ocap/store'; -import { describe, it, expect, vi } from 'vitest'; - -import { loggingMiddleware, logger } from './logging.ts'; - -describe('loggingMiddleware', () => { - const mockKernelDatabase = {} as unknown as KernelDatabase; - const mockKernel = {} as unknown as Kernel; - - it('should call the next function with the provided arguments', async () => { - const next = vi.fn(); - const middleware = loggingMiddleware(next); - const params = { arg1: 'arg1', arg2: 'arg2' }; - await middleware(mockKernel, mockKernelDatabase, params); - expect(next).toHaveBeenCalledWith(mockKernel, mockKernelDatabase, params); - }); - - it('should return the result from the next function', async () => { - const expectedResult = 'test result'; - const next = vi.fn().mockResolvedValue(expectedResult); - const middleware = loggingMiddleware(next); - const result = await middleware(mockKernel, mockKernelDatabase, {}); - expect(result).toBe(expectedResult); - }); - - it('should log the execution duration', async () => { - const debugSpy = vi.spyOn(logger, 'debug'); - const next = vi.fn().mockImplementation( - async () => - new Promise((resolve) => { - setTimeout(resolve, 50); - }), - ); - const middleware = loggingMiddleware(next); - await middleware(mockKernel, mockKernelDatabase, {}); - expect(debugSpy).toHaveBeenCalledWith( - expect.stringMatching(/Command executed in \d*\.?\d+ms/u), - ); - }); - - it('should log duration even if next function throws', async () => { - const debugSpy = vi.spyOn(logger, 'debug'); - const error = new Error('Test error'); - const next = vi.fn().mockRejectedValue(error); - const middleware = loggingMiddleware(next); - await expect( - middleware(mockKernel, mockKernelDatabase, {}), - ).rejects.toThrow(error); - expect(debugSpy).toHaveBeenCalledWith( - expect.stringMatching(/Command executed in \d*\.?\d+ms/u), - ); - }); -}); diff --git a/packages/extension/src/kernel-integration/middlewares/logging.ts b/packages/extension/src/kernel-integration/middlewares/logging.ts deleted file mode 100644 index 60cdee840..000000000 --- a/packages/extension/src/kernel-integration/middlewares/logging.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { makeLogger } from '@ocap/utils'; - -import type { Middleware } from '../command-registry.ts'; - -export const logger = makeLogger('[kernel-commands]'); - -export const loggingMiddleware: Middleware = - (next) => - async (...args) => { - const start = performance.now(); - try { - return await next(...args); - } finally { - const duration = performance.now() - start; - logger.debug(`Command executed in ${duration}ms`); - } - }; diff --git a/packages/extension/src/kernel-integration/ui-connections.test.ts b/packages/extension/src/kernel-integration/ui-connections.test.ts index 8b8d90330..24f42396f 100644 --- a/packages/extension/src/kernel-integration/ui-connections.test.ts +++ b/packages/extension/src/kernel-integration/ui-connections.test.ts @@ -1,9 +1,10 @@ +import type { JsonRpcRequest, JsonRpcResponse } from '@metamask/utils'; import type { PostMessageTarget } from '@ocap/streams/browser'; import { delay } from '@ocap/test-utils'; import { TestDuplexStream } from '@ocap/test-utils/streams'; import { describe, it, expect, vi, beforeEach } from 'vitest'; -import type { KernelControlCommand, KernelControlReply } from './messages.ts'; +import type { KernelControlReply } from './messages.ts'; import { establishKernelConnection, receiveUiConnections, @@ -162,12 +163,12 @@ describe('ui-connections', () => { const logger = makeMockLogger(); const mockHandleMessage = vi.fn( - async (_message: KernelControlCommand): Promise => ({ + async ( + _request: JsonRpcRequest, + ): Promise> => ({ id: 'foo', - payload: { - method: 'getStatus', - params: { vats: [], clusterConfig }, - }, + jsonrpc: '2.0' as const, + result: { vats: [], clusterConfig }, }), ); diff --git a/packages/extension/src/kernel-integration/ui-connections.ts b/packages/extension/src/kernel-integration/ui-connections.ts index 507b77fdf..cfc1ec3c0 100644 --- a/packages/extension/src/kernel-integration/ui-connections.ts +++ b/packages/extension/src/kernel-integration/ui-connections.ts @@ -1,34 +1,28 @@ +import { isJsonRpcRequest, isJsonRpcResponse } from '@metamask/utils'; +import type { JsonRpcRequest, JsonRpcResponse } from '@metamask/utils'; import { PostMessageDuplexStream } from '@ocap/streams/browser'; import { stringify } from '@ocap/utils'; import type { Logger } from '@ocap/utils'; import { nanoid } from 'nanoid'; -import { - isKernelControlCommand, - isKernelControlReply, - isUiControlCommand, -} from './messages.ts'; -import type { - KernelControlCommand, - KernelControlReply, - UiControlCommand, -} from './messages.ts'; +import { isUiControlCommand } from './messages.ts'; +import type { KernelControlReply, UiControlCommand } from './messages.ts'; export const UI_CONTROL_CHANNEL_NAME = 'ui-control'; export type KernelControlStream = PostMessageDuplexStream< - KernelControlCommand, - KernelControlReply + JsonRpcRequest, + JsonRpcResponse >; export type KernelControlReplyStream = PostMessageDuplexStream< - KernelControlReply, - KernelControlCommand + JsonRpcResponse, + JsonRpcRequest >; type HandleInstanceMessage = ( - message: KernelControlCommand, -) => Promise; + request: JsonRpcRequest, +) => Promise>; /** * Establishes a connection between a UI instance and the kernel. Should be called @@ -50,10 +44,10 @@ export const establishKernelConnection = async ( } as UiControlCommand); const kernelStream = await PostMessageDuplexStream.make< - KernelControlReply, - KernelControlCommand + JsonRpcResponse, + JsonRpcRequest >({ - validateInput: isKernelControlReply, + validateInput: isJsonRpcResponse, messageTarget: instanceChannel, onEnd: () => { instanceChannel.close(); @@ -81,7 +75,7 @@ const connectToNextUiInstance = async ( const instanceChannel = new BroadcastChannel(channelName); const instanceStream: KernelControlStream = await PostMessageDuplexStream.make({ - validateInput: isKernelControlCommand, + validateInput: isJsonRpcRequest, messageTarget: instanceChannel, onEnd: () => { instanceChannel.close(); diff --git a/packages/extension/src/ui/context/PanelContext.test.tsx b/packages/extension/src/ui/context/PanelContext.test.tsx index 8a908d7aa..417a5c20f 100644 --- a/packages/extension/src/ui/context/PanelContext.test.tsx +++ b/packages/extension/src/ui/context/PanelContext.test.tsx @@ -11,8 +11,8 @@ vi.mock('../services/logger.ts', () => ({ }, })); -vi.mock('../utils.ts', () => ({ - isErrorResponse: vi.fn(), +vi.mock('@metamask/utils', () => ({ + isJsonRpcFailure: vi.fn(), })); vi.mock('../hooks/useStatusPolling.ts', () => ({ @@ -30,9 +30,9 @@ describe('PanelContext', () => { const payload = { test: 'data' }; const response = { success: true }; mockSendMessage.mockResolvedValueOnce(response); - vi.mocked(await import('../utils.ts')).isErrorResponse.mockReturnValue( - false, - ); + vi.mocked( + await import('@metamask/utils'), + ).isJsonRpcFailure.mockReturnValue(false); const { result } = renderHook(() => usePanelContext(), { wrapper: ({ children }) => ( @@ -53,9 +53,9 @@ describe('PanelContext', () => { const payload = { test: 'data' }; const errorResponse = { error: 'Test error' }; mockSendMessage.mockResolvedValueOnce(errorResponse); - vi.mocked(await import('../utils.ts')).isErrorResponse.mockReturnValue( - true, - ); + vi.mocked( + await import('@metamask/utils'), + ).isJsonRpcFailure.mockReturnValue(true); const { result } = renderHook(() => usePanelContext(), { wrapper: ({ children }) => ( diff --git a/packages/extension/src/ui/context/PanelContext.tsx b/packages/extension/src/ui/context/PanelContext.tsx index 15a3614a6..1862201b6 100644 --- a/packages/extension/src/ui/context/PanelContext.tsx +++ b/packages/extension/src/ui/context/PanelContext.tsx @@ -1,3 +1,4 @@ +import { isJsonRpcFailure } from '@metamask/utils'; import { stringify } from '@ocap/utils'; import { createContext, @@ -12,7 +13,6 @@ import type { KernelStatus } from '../../kernel-integration/messages.ts'; import { useStatusPolling } from '../hooks/useStatusPolling.ts'; import { logger } from '../services/logger.ts'; import type { SendMessageFunction } from '../services/stream.ts'; -import { isErrorResponse } from '../utils.ts'; export type OutputType = 'sent' | 'received' | 'error' | 'success'; @@ -71,7 +71,7 @@ export const PanelProvider: React.FC<{ logMessage(stringify(payload, 2), 'sent'); const response = await sendMessage(payload); - if (isErrorResponse(response)) { + if (isJsonRpcFailure(response)) { throw new Error(stringify(response.error, 0)); } return response; diff --git a/packages/extension/src/ui/hooks/useDatabaseInspector.test.ts b/packages/extension/src/ui/hooks/useDatabaseInspector.test.ts index 34b98d2b7..846b676d5 100644 --- a/packages/extension/src/ui/hooks/useDatabaseInspector.test.ts +++ b/packages/extension/src/ui/hooks/useDatabaseInspector.test.ts @@ -3,16 +3,11 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { useDatabaseInspector } from './useDatabaseInspector.ts'; import { usePanelContext } from '../context/PanelContext.tsx'; -import { isErrorResponse } from '../utils.ts'; vi.mock('../context/PanelContext.tsx', () => ({ usePanelContext: vi.fn(), })); -vi.mock('../utils.ts', () => ({ - isErrorResponse: vi.fn(), -})); - vi.mock('@ocap/utils', () => ({ stringify: JSON.stringify, })); @@ -45,7 +40,6 @@ describe('useDatabaseInspector', () => { it('should fetch tables on mount', async () => { const mockTables = [{ name: 'table1' }, { name: 'table2' }]; mockSendMessage.mockResolvedValueOnce(mockTables); - vi.mocked(isErrorResponse).mockReturnValue(false); renderHook(() => useDatabaseInspector()); expect(mockSendMessage).toHaveBeenCalledWith({ method: 'executeDBQuery', @@ -59,7 +53,6 @@ describe('useDatabaseInspector', () => { const { result } = renderHook(() => useDatabaseInspector()); const mockTableData = [{ id: 1, name: 'test' }]; mockSendMessage.mockResolvedValueOnce(mockTableData); - vi.mocked(isErrorResponse).mockReturnValue(false); await act(async () => { result.current.setSelectedTable('testTable'); }); @@ -73,7 +66,6 @@ describe('useDatabaseInspector', () => { it('should set first table as selected when tables are fetched', async () => { const mockTables = [{ name: 'table1' }, { name: 'table2' }]; mockSendMessage.mockResolvedValueOnce(mockTables); - vi.mocked(isErrorResponse).mockReturnValue(false); const { result } = renderHook(() => useDatabaseInspector()); await waitFor(() => { expect(result.current.selectedTable).toBe('table1'); @@ -86,7 +78,6 @@ describe('useDatabaseInspector', () => { const { result } = renderHook(() => useDatabaseInspector()); const mockQueryResult = [{ id: 1, value: 'test' }]; mockSendMessage.mockResolvedValueOnce(mockQueryResult); - vi.mocked(isErrorResponse).mockReturnValue(false); await act(async () => { result.current.executeQuery('SELECT * FROM test'); }); @@ -117,7 +108,6 @@ describe('useDatabaseInspector', () => { const { result } = renderHook(() => useDatabaseInspector()); const errorResponse = { error: 'Invalid query' }; mockSendMessage.mockResolvedValueOnce(errorResponse); - vi.mocked(isErrorResponse).mockReturnValue(true); await act(async () => { result.current.executeQuery('SELECT * FROM test'); }); @@ -130,7 +120,6 @@ describe('useDatabaseInspector', () => { const { result } = renderHook(() => useDatabaseInspector()); const mockTableData = [{ id: 1, name: 'test' }]; mockSendMessage.mockResolvedValueOnce(mockTableData); - vi.mocked(isErrorResponse).mockReturnValue(false); await act(async () => { result.current.setSelectedTable('testTable'); }); diff --git a/packages/extension/src/ui/hooks/useDatabaseInspector.ts b/packages/extension/src/ui/hooks/useDatabaseInspector.ts index f87cf6f5d..b51a6d5b5 100644 --- a/packages/extension/src/ui/hooks/useDatabaseInspector.ts +++ b/packages/extension/src/ui/hooks/useDatabaseInspector.ts @@ -1,8 +1,8 @@ +import { hasProperty } from '@metamask/utils'; import { stringify } from '@ocap/utils'; import { useCallback, useEffect, useState } from 'react'; import { usePanelContext } from '../context/PanelContext.tsx'; -import { isErrorResponse } from '../utils.ts'; /** * Hook for the database inspector. @@ -31,7 +31,7 @@ export function useDatabaseInspector(): { }) .then((result) => { logMessage(stringify(result, 0), 'received'); - if (!isErrorResponse(result)) { + if (!hasProperty(result, 'error')) { setTableData(result); } return result; @@ -49,8 +49,8 @@ export function useDatabaseInspector(): { method: 'executeDBQuery', params: { sql: "SELECT name FROM sqlite_master WHERE type='table'" }, }); - if (!isErrorResponse(result)) { - logMessage(stringify(result, 0), 'received'); + logMessage(stringify(result, 0), 'received'); + if (!hasProperty(result, 'error')) { const tableNames = result .map((row: Record) => row.name) .filter((name): name is string => name !== undefined); @@ -68,8 +68,8 @@ export function useDatabaseInspector(): { method: 'executeDBQuery', params: { sql: `SELECT * FROM ${tableName}` }, }); - if (!isErrorResponse(result)) { - logMessage(stringify(result, 0), 'received'); + logMessage(stringify(result, 0), 'received'); + if (!hasProperty(result, 'error')) { setTableData(result); } }, diff --git a/packages/extension/src/ui/hooks/useKernelActions.test.ts b/packages/extension/src/ui/hooks/useKernelActions.test.ts index 01e26eafc..7b95cedce 100644 --- a/packages/extension/src/ui/hooks/useKernelActions.test.ts +++ b/packages/extension/src/ui/hooks/useKernelActions.test.ts @@ -33,13 +33,13 @@ describe('useKernelActions', () => { it('sends message with payload', async () => { const { useKernelActions } = await import('./useKernelActions.ts'); const { result } = renderHook(() => useKernelActions()); - const expectedPayload = { test: 'content' }; + const expectedParams = { test: 'content' }; mockSendMessage.mockResolvedValueOnce({ success: true }); result.current.sendKernelCommand(); await waitFor(() => { expect(mockSendMessage).toHaveBeenCalledWith({ method: 'sendVatCommand', - params: expectedPayload, + params: expectedParams, }); }); }); @@ -85,7 +85,7 @@ describe('useKernelActions', () => { await waitFor(() => { expect(mockSendMessage).toHaveBeenCalledWith({ method: 'terminateAllVats', - params: null, + params: [], }); }); expect(mockLogMessage).toHaveBeenCalledWith( @@ -121,7 +121,7 @@ describe('useKernelActions', () => { await waitFor(() => { expect(mockSendMessage).toHaveBeenCalledWith({ method: 'clearState', - params: null, + params: [], }); }); expect(mockLogMessage).toHaveBeenCalledWith('State cleared', 'success'); @@ -154,7 +154,7 @@ describe('useKernelActions', () => { await waitFor(() => { expect(mockSendMessage).toHaveBeenCalledWith({ method: 'reload', - params: null, + params: [], }); }); expect(mockLogMessage).toHaveBeenCalledWith( diff --git a/packages/extension/src/ui/hooks/useKernelActions.ts b/packages/extension/src/ui/hooks/useKernelActions.ts index 3c6bfc9aa..bbef6d259 100644 --- a/packages/extension/src/ui/hooks/useKernelActions.ts +++ b/packages/extension/src/ui/hooks/useKernelActions.ts @@ -36,7 +36,7 @@ export function useKernelActions(): { const terminateAllVats = useCallback(() => { sendMessage({ method: 'terminateAllVats', - params: null, + params: [], }) .then(() => logMessage('All vats terminated', 'success')) .catch(() => logMessage('Failed to terminate all vats', 'error')); @@ -48,7 +48,7 @@ export function useKernelActions(): { const clearState = useCallback(() => { sendMessage({ method: 'clearState', - params: null, + params: [], }) .then(() => logMessage('State cleared', 'success')) .catch(() => logMessage('Failed to clear state', 'error')); @@ -60,7 +60,7 @@ export function useKernelActions(): { const reload = useCallback(() => { sendMessage({ method: 'reload', - params: null, + params: [], }) .then(() => logMessage('Default sub-cluster reloaded', 'success')) .catch(() => logMessage('Failed to reload', 'error')); diff --git a/packages/extension/src/ui/hooks/useStatusPolling.test.ts b/packages/extension/src/ui/hooks/useStatusPolling.test.ts index 697c86e7b..9380e57a8 100644 --- a/packages/extension/src/ui/hooks/useStatusPolling.test.ts +++ b/packages/extension/src/ui/hooks/useStatusPolling.test.ts @@ -9,10 +9,6 @@ vi.mock('../services/logger.ts', () => ({ }, })); -vi.mock('../utils.ts', () => ({ - isErrorResponse: vi.fn(), -})); - describe('useStatusPolling', () => { const mockSendMessage = vi.fn(); const mockInterval = 100; @@ -25,16 +21,13 @@ describe('useStatusPolling', () => { it('should start polling and fetch initial status', async () => { const mockStatus = { vats: [], clusterConfig }; mockSendMessage.mockResolvedValueOnce(mockStatus); - vi.mocked(await import('../utils.ts')).isErrorResponse.mockReturnValue( - false, - ); const { useStatusPolling } = await import('./useStatusPolling.ts'); const { result } = renderHook(() => useStatusPolling(mockSendMessage, mockIsRequestInProgress, mockInterval), ); expect(mockSendMessage).toHaveBeenCalledWith({ method: 'getStatus', - params: null, + params: [], }); await waitFor(() => expect(result.current).toStrictEqual(mockStatus)); }); @@ -43,19 +36,19 @@ describe('useStatusPolling', () => { const { useStatusPolling } = await import('./useStatusPolling.ts'); const errorResponse = { error: 'Test error' }; mockSendMessage.mockResolvedValueOnce(errorResponse); - vi.mocked(await import('../utils.ts')).isErrorResponse.mockReturnValue( - true, - ); renderHook(() => useStatusPolling(mockSendMessage, mockIsRequestInProgress, mockInterval), ); expect(mockSendMessage).toHaveBeenCalledWith({ method: 'getStatus', - params: null, + params: [], }); expect( vi.mocked(await import('../services/logger.ts')).logger.error, - ).toHaveBeenCalledWith('Failed to fetch status:', new Error('Test error')); + ).toHaveBeenCalledWith( + 'Failed to fetch status:', + new Error('"Test error"'), + ); }); it('should handle fetch errors', async () => { @@ -67,7 +60,7 @@ describe('useStatusPolling', () => { ); expect(mockSendMessage).toHaveBeenCalledWith({ method: 'getStatus', - params: null, + params: [], }); expect( vi.mocked(await import('../services/logger.ts')).logger.error, @@ -100,9 +93,6 @@ describe('useStatusPolling', () => { const { useStatusPolling } = await import('./useStatusPolling.ts'); const mockStatus = { vats: [], clusterConfig }; mockSendMessage.mockResolvedValue(mockStatus); - vi.mocked(await import('../utils.ts')).isErrorResponse.mockReturnValue( - false, - ); renderHook(() => useStatusPolling( mockSendMessage, @@ -121,9 +111,6 @@ describe('useStatusPolling', () => { const { useStatusPolling } = await import('./useStatusPolling.ts'); const mockStatus = { vats: [] }; mockSendMessage.mockResolvedValue(mockStatus); - vi.mocked(await import('../utils.ts')).isErrorResponse.mockReturnValue( - false, - ); const { unmount } = renderHook(() => useStatusPolling( mockSendMessage, diff --git a/packages/extension/src/ui/hooks/useStatusPolling.ts b/packages/extension/src/ui/hooks/useStatusPolling.ts index 0fed482dc..4617137bc 100644 --- a/packages/extension/src/ui/hooks/useStatusPolling.ts +++ b/packages/extension/src/ui/hooks/useStatusPolling.ts @@ -1,9 +1,10 @@ +import { hasProperty } from '@metamask/utils'; +import { stringify } from '@ocap/utils'; import { useEffect, useRef, useState } from 'react'; import type { StreamState } from './useStream.ts'; import type { KernelStatus } from '../../kernel-integration/messages.ts'; import { logger } from '../services/logger.ts'; -import { isErrorResponse } from '../utils.ts'; /** * Hook to start polling for kernel status @@ -32,10 +33,10 @@ export const useStatusPolling = ( try { const result = await sendMessage({ method: 'getStatus', - params: null, + params: [], }); - if (isErrorResponse(result)) { - throw new Error(result.error); + if (hasProperty(result, 'error')) { + throw new Error(stringify(result.error, 0)); } setStatus(result); } catch (error) { diff --git a/packages/extension/src/ui/hooks/useVats.test.ts b/packages/extension/src/ui/hooks/useVats.test.ts index d1d245392..13934252d 100644 --- a/packages/extension/src/ui/hooks/useVats.test.ts +++ b/packages/extension/src/ui/hooks/useVats.test.ts @@ -96,7 +96,7 @@ describe('useVats', () => { id: mockVatId, payload: { method: 'ping', - params: null, + params: [], }, }, }); diff --git a/packages/extension/src/ui/hooks/useVats.ts b/packages/extension/src/ui/hooks/useVats.ts index 1f192d4a4..7b798a4f5 100644 --- a/packages/extension/src/ui/hooks/useVats.ts +++ b/packages/extension/src/ui/hooks/useVats.ts @@ -45,7 +45,7 @@ export const useVats = (): { id, payload: { method: VatCommandMethod.ping, - params: null, + params: [], }, }, }) diff --git a/packages/extension/src/ui/services/stream.ts b/packages/extension/src/ui/services/stream.ts index df6cedc0d..03a8e2bd4 100644 --- a/packages/extension/src/ui/services/stream.ts +++ b/packages/extension/src/ui/services/stream.ts @@ -1,3 +1,4 @@ +import { isJsonRpcSuccess } from '@metamask/utils'; import { MessageResolver } from '@ocap/kernel'; import { logger } from './logger.ts'; @@ -9,7 +10,7 @@ import type { import { establishKernelConnection } from '../../kernel-integration/ui-connections.ts'; export type SendMessageFunction = ( - payload: Extract, + command: Extract, ) => Promise; /** @@ -34,8 +35,16 @@ export async function setupStream(): Promise<{ window.addEventListener('unload', cleanup); kernelStream - .drain(async ({ id, payload }) => { - resolver.handleResponse(id, payload.params); + .drain(async (response) => { + if (typeof response.id !== 'string') { + throw new Error('Invalid response id'); + } + + if (isJsonRpcSuccess(response)) { + resolver.handleResponse(response.id, response.result); + } else { + resolver.handleResponse(response.id, response); + } }) .catch((error) => { logger.error('error draining kernel stream', error); @@ -46,9 +55,10 @@ export async function setupStream(): Promise<{ logger.log('sending message', payload); return resolver.createMessage(async (messageId) => { await kernelStream.write({ + ...payload, id: messageId, - payload, - } as KernelControlCommand); + jsonrpc: '2.0', + }); }); }; diff --git a/packages/extension/src/ui/utils.ts b/packages/extension/src/ui/utils.ts index d67a6be4b..42b22c8a6 100644 --- a/packages/extension/src/ui/utils.ts +++ b/packages/extension/src/ui/utils.ts @@ -16,17 +16,3 @@ export function isValidBundleUrl(url?: string): boolean { return false; } } - -type ErrorResponse = { - error: unknown; -}; - -/** - * Checks if a value is an error response. - * - * @param value - The value to check. - * @returns Whether the value is an error response. - */ -export function isErrorResponse(value: unknown): value is ErrorResponse { - return typeof value === 'object' && value !== null && 'error' in value; -} diff --git a/packages/extension/src/vats/default-cluster.json b/packages/extension/src/vats/default-cluster.json index fdd917ff3..fd658b18a 100644 --- a/packages/extension/src/vats/default-cluster.json +++ b/packages/extension/src/vats/default-cluster.json @@ -1,6 +1,7 @@ { "bootstrap": "alice", "forceReset": true, + "bundles": null, "vats": { "alice": { "bundleSpec": "http://localhost:3000/sample-vat.bundle", diff --git a/packages/extension/src/vats/minimal-cluster.json b/packages/extension/src/vats/minimal-cluster.json index 5a210e028..fc65a29c1 100644 --- a/packages/extension/src/vats/minimal-cluster.json +++ b/packages/extension/src/vats/minimal-cluster.json @@ -1,6 +1,7 @@ { "bootstrap": "main", "forceReset": true, + "bundles": null, "vats": { "main": { "bundleSpec": "http://localhost:3000/empty-vat.bundle", diff --git a/packages/extension/test/e2e/vat-manager.test.ts b/packages/extension/test/e2e/vat-manager.test.ts index dfdd56619..69a916027 100644 --- a/packages/extension/test/e2e/vat-manager.test.ts +++ b/packages/extension/test/e2e/vat-manager.test.ts @@ -180,7 +180,7 @@ test.describe('Vat Manager', () => { "id": "v1", "payload": { "method": "ping", - "params": null + "params": [] } }`, ); diff --git a/packages/extension/tsconfig.build.json b/packages/extension/tsconfig.build.json index 24d929890..ae4bb837a 100644 --- a/packages/extension/tsconfig.build.json +++ b/packages/extension/tsconfig.build.json @@ -13,11 +13,11 @@ "plugins": [{ "name": "typescript-plugin-css-modules" }] }, "references": [ + { "path": "../kernel/tsconfig.build.json" }, { "path": "../shims/tsconfig.build.json" }, + { "path": "../store/tsconfig.build.json" }, { "path": "../streams/tsconfig.build.json" }, - { "path": "../kernel/tsconfig.build.json" }, - { "path": "../utils/tsconfig.build.json" }, - { "path": "../store/tsconfig.build.json" } + { "path": "../utils/tsconfig.build.json" } ], "include": [ "./src/**/*.ts", diff --git a/packages/kernel-test/src/liveslots.test.ts b/packages/kernel-test/src/liveslots.test.ts index 747f9561d..0b0803386 100644 --- a/packages/kernel-test/src/liveslots.test.ts +++ b/packages/kernel-test/src/liveslots.test.ts @@ -1,6 +1,7 @@ import '@ocap/shims/endoify'; import { makePromiseKit } from '@endo/promise-kit'; import { Kernel } from '@ocap/kernel'; +import type { ClusterConfig } from '@ocap/kernel'; import { MessagePort as NodeMessagePort, MessageChannel as NodeMessageChannel, @@ -18,31 +19,35 @@ process.stdout.write = (buffer: string, encoding, callback): void => { origStdoutWrite(buffer, encoding, callback); }; -const testSubcluster = { +const makeTestSubcluster = ( + testName: string, + bundleSpec: string, +): ClusterConfig => ({ bootstrap: 'alice', forceReset: true, + bundles: null, vats: { alice: { - bundleSpec: 'bundle name', + bundleSpec, parameters: { name: 'Alice', - test: 'put the test name here', + test: testName, }, }, bob: { - bundleSpec: 'bundle name', + bundleSpec, parameters: { name: 'Bob', }, }, carol: { - bundleSpec: 'bundle name', + bundleSpec, parameters: { name: 'Carol', }, }, }, -}; +}); describe('liveslots promise handling', () => { let kernel: Kernel; @@ -73,11 +78,9 @@ describe('liveslots promise handling', () => { `${bundleName}.bundle`, import.meta.url, ).toString(); - testSubcluster.vats.alice.parameters.test = testName; - testSubcluster.vats.alice.bundleSpec = bundleSpec; - testSubcluster.vats.bob.bundleSpec = bundleSpec; - testSubcluster.vats.carol.bundleSpec = bundleSpec; - const bootstrapResultRaw = await kernel.launchSubcluster(testSubcluster); + const bootstrapResultRaw = await kernel.launchSubcluster( + makeTestSubcluster(testName, bundleSpec), + ); const { promise, resolve } = makePromiseKit(); setTimeout(() => resolve(null), 1000); diff --git a/packages/kernel-test/src/vatstore.test.ts b/packages/kernel-test/src/vatstore.test.ts index bcfa2887a..b4fba56d6 100644 --- a/packages/kernel-test/src/vatstore.test.ts +++ b/packages/kernel-test/src/vatstore.test.ts @@ -31,9 +31,10 @@ function bundleSpec(bundleName: string): string { return new URL(`${bundleName}.bundle`, import.meta.url).toString(); } -const testSubcluster = { +const makeTestSubcluster = (): ClusterConfig => ({ bootstrap: 'alice', forceReset: true, + bundles: null, vats: { alice: { bundleSpec: bundleSpec('vatstore-vat'), @@ -54,7 +55,7 @@ const testSubcluster = { }, }, }, -}; +}); /** * Handle all the boilerplate to set up a kernel instance. @@ -205,7 +206,7 @@ describe('exercise vatstore', async () => { }, ); const kernel = await makeKernel(kernelDatabase, true); - await runTestVats(kernel, testSubcluster); + await runTestVats(kernel, makeTestSubcluster()); type VSRecord = { key: string; value: string }; const vsContents = kernelDatabase.executeQuery( diff --git a/packages/kernel/src/Kernel.test.ts b/packages/kernel/src/Kernel.test.ts index 647715033..dc6a8c7e9 100644 --- a/packages/kernel/src/Kernel.test.ts +++ b/packages/kernel/src/Kernel.test.ts @@ -34,6 +34,8 @@ describe('Kernel', () => { }); const makeMockClusterConfig = (): ClusterConfig => ({ bootstrap: 'alice', + forceReset: null, + bundles: null, vats: { alice: { bundleSpec: 'http://localhost:3000/sample-vat.bundle', diff --git a/packages/kernel/src/VatHandle.test.ts b/packages/kernel/src/VatHandle.test.ts index 9e71fa392..92eef54fb 100644 --- a/packages/kernel/src/VatHandle.test.ts +++ b/packages/kernel/src/VatHandle.test.ts @@ -55,18 +55,14 @@ describe('VatHandle', () => { mockKernelStore = makeKernelStore(makeMapKernelDatabase()); sendVatCommandMock = vi .spyOn(VatHandle.prototype, 'sendVatCommand') - .mockResolvedValueOnce('fake') .mockResolvedValueOnce('fake'); }); describe('init', () => { - it('initializes the vat and sends ping & initVat messages', async () => { + it('initializes the vat and sends initVat message', async () => { await makeVat(); - expect(sendVatCommandMock).toHaveBeenCalledWith({ - method: VatCommandMethod.ping, - params: null, - }); + expect(sendVatCommandMock).toHaveBeenCalledTimes(1); expect(sendVatCommandMock).toHaveBeenCalledWith({ method: VatCommandMethod.initVat, params: { @@ -96,7 +92,7 @@ describe('VatHandle', () => { const { vat } = await makeVat(); const mockMessage = { method: VatCommandMethod.ping, - params: null, + params: [], } as VatCommand['payload']; const sendVatCommandPromise = vat.sendVatCommand(mockMessage); @@ -127,7 +123,7 @@ describe('VatHandle', () => { // Create a pending message first const messagePromise = vat.sendVatCommand({ method: VatCommandMethod.ping, - params: null, + params: [], }); // Handle the response @@ -145,7 +141,7 @@ describe('VatHandle', () => { // Create a pending message that should be rejected on terminate const messagePromise = vat.sendVatCommand({ method: VatCommandMethod.ping, - params: null, + params: [], }); await vat.terminate(); diff --git a/packages/kernel/src/VatHandle.ts b/packages/kernel/src/VatHandle.ts index d8b269c8b..32e111414 100644 --- a/packages/kernel/src/VatHandle.ts +++ b/packages/kernel/src/VatHandle.ts @@ -141,13 +141,6 @@ export class VatHandle { }, ); - // XXX This initial `ping` was originally put here as a sanity check to make - // sure that the vat was actually running and able to exchange message - // traffic with the kernel, but the addition of the `initVat` message to the - // startup flow has, I'm fairly sure, obviated the need for that as it - // effectively performs the same function. Probably this ping should be - // removed. - await this.sendVatCommand({ method: VatCommandMethod.ping, params: null }); await this.sendVatCommand({ method: VatCommandMethod.initVat, params: { vatConfig: this.config, state: this.#vatStore.getKVData() }, diff --git a/packages/kernel/src/VatSupervisor.test.ts b/packages/kernel/src/VatSupervisor.test.ts index fbcc27912..f46e4e945 100644 --- a/packages/kernel/src/VatSupervisor.test.ts +++ b/packages/kernel/src/VatSupervisor.test.ts @@ -71,7 +71,7 @@ describe('VatSupervisor', () => { await supervisor.handleMessage({ id: 'v0:0', - payload: { method: VatCommandMethod.ping, params: null }, + payload: { method: VatCommandMethod.ping, params: [] }, }); expect(replySpy).toHaveBeenCalledWith('v0:0', { diff --git a/packages/kernel/src/messages/kernel.ts b/packages/kernel/src/messages/kernel.ts index 01f426712..be421c1f8 100644 --- a/packages/kernel/src/messages/kernel.ts +++ b/packages/kernel/src/messages/kernel.ts @@ -1,5 +1,6 @@ import { object, union, is } from '@metamask/superstruct'; -import type { Infer } from '@metamask/superstruct'; +import type { Infer, Struct } from '@metamask/superstruct'; +import type { Json } from '@metamask/utils'; import type { TypeGuard } from '@ocap/utils'; import { @@ -14,9 +15,25 @@ export const KernelCommandMethod = { ping: VatTestCommandMethod.ping, } as const; -const KernelCommandStruct = union([VatTestMethodStructs.ping]); - -const KernelCommandReplyStruct = union([VatTestReplyStructs.ping]); +// Explicitly annotated due to a TS2742 error that occurs during CommonJS +// builds by ts-bridge. +const KernelCommandStruct = union([VatTestMethodStructs.ping]) as Struct< + { + method: 'ping'; + params: Json[]; + }, + null +>; + +// Explicitly annotated due to a TS2742 error that occurs during CommonJS +// builds by ts-bridge. +const KernelCommandReplyStruct = union([VatTestReplyStructs.ping]) as Struct< + { + method: 'ping'; + params: string; + }, + null +>; export type KernelCommand = Infer; export type KernelCommandReply = Infer; diff --git a/packages/kernel/src/messages/vat.test.ts b/packages/kernel/src/messages/vat.test.ts index 401d0b2c1..a881e6b0d 100644 --- a/packages/kernel/src/messages/vat.test.ts +++ b/packages/kernel/src/messages/vat.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'; import { isVatCommand, isVatCommandReply, VatCommandMethod } from './vat.ts'; describe('isVatCommand', () => { - const payload = { method: VatCommandMethod.ping, params: null }; + const payload = { method: VatCommandMethod.ping, params: [] }; it.each` value | expectedResult | description diff --git a/packages/kernel/src/messages/vat.ts b/packages/kernel/src/messages/vat.ts index 4a3820649..1a54945ef 100644 --- a/packages/kernel/src/messages/vat.ts +++ b/packages/kernel/src/messages/vat.ts @@ -12,7 +12,9 @@ import { boolean, is, } from '@metamask/superstruct'; -import type { Infer } from '@metamask/superstruct'; +import type { Infer, Struct } from '@metamask/superstruct'; +import type { Json } from '@metamask/utils'; +import { EmptyJsonArray } from '@ocap/utils'; import { isVatId, @@ -45,12 +47,29 @@ export const VatCommandMethod = { const VatMessageIdStruct = refine(string(), 'VatMessageId', isVatMessageId); +/** + * This type only exists due to a TS2742 error that occurs during CommonJS + * builds by ts-bridge. + */ +export type VatTestMethodStructs = { + readonly ping: Struct< + { + method: 'ping'; + params: Json[]; + }, + { + method: Struct<'ping', 'ping'>; + params: Struct>; + } + >; +}; + export const VatTestMethodStructs = { [VatCommandMethod.ping]: object({ method: literal(VatCommandMethod.ping), - params: literal(null), + params: EmptyJsonArray, }), -} as const; +} as VatTestMethodStructs; const VatOneResolutionStruct = tuple([string(), boolean(), CapDataStruct]); diff --git a/packages/kernel/src/types.ts b/packages/kernel/src/types.ts index 5d99ed44e..019876812 100644 --- a/packages/kernel/src/types.ts +++ b/packages/kernel/src/types.ts @@ -17,6 +17,7 @@ import { set, literal, boolean, + nullable, } from '@metamask/superstruct'; import type { Infer } from '@metamask/superstruct'; import type { Json } from '@metamask/utils'; @@ -271,9 +272,9 @@ export type VatConfigTable = Record; export const ClusterConfigStruct = object({ bootstrap: string(), - forceReset: optional(boolean()), + forceReset: nullable(boolean()), vats: record(string(), VatConfigStruct), - bundles: optional(record(string(), VatConfigStruct)), + bundles: nullable(record(string(), VatConfigStruct)), }); export type ClusterConfig = Infer; diff --git a/packages/kernel/tsconfig.build.json b/packages/kernel/tsconfig.build.json index f0e2effbf..4944f89ce 100644 --- a/packages/kernel/tsconfig.build.json +++ b/packages/kernel/tsconfig.build.json @@ -8,10 +8,10 @@ "types": ["ses"] }, "references": [ - { "path": "../streams/tsconfig.build.json" }, - { "path": "../utils/tsconfig.build.json" }, { "path": "../errors/tsconfig.build.json" }, - { "path": "../store/tsconfig.build.json" } + { "path": "../streams/tsconfig.build.json" }, + { "path": "../store/tsconfig.build.json" }, + { "path": "../utils/tsconfig.build.json" } ], "include": ["./src"] } diff --git a/packages/kernel/tsconfig.json b/packages/kernel/tsconfig.json index dce592358..3a6f0cb5c 100644 --- a/packages/kernel/tsconfig.json +++ b/packages/kernel/tsconfig.json @@ -5,10 +5,10 @@ }, "references": [ { "path": "../errors" }, + { "path": "../store" }, { "path": "../streams" }, { "path": "../utils" }, - { "path": "../test-utils" }, - { "path": "../store" } + { "path": "../test-utils" } ], "include": ["./src", "./test", "./vite.config.ts", "./vitest.config.ts"] } diff --git a/packages/nodejs/test/e2e/kernel-worker.test.ts b/packages/nodejs/test/e2e/kernel-worker.test.ts index 98dd49927..81b6587f9 100644 --- a/packages/nodejs/test/e2e/kernel-worker.test.ts +++ b/packages/nodejs/test/e2e/kernel-worker.test.ts @@ -78,7 +78,7 @@ describe('Kernel Worker', () => { async (vatId: VatId) => await kernel.sendVatCommand(vatId, { method: VatCommandMethod.ping, - params: null, + params: [], }), ), ); diff --git a/packages/store/tsconfig.build.json b/packages/store/tsconfig.build.json index d5a0ec2d4..3023abfed 100644 --- a/packages/store/tsconfig.build.json +++ b/packages/store/tsconfig.build.json @@ -5,8 +5,7 @@ "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src", - "types": ["ses"], - "composite": true + "types": ["ses"] }, "references": [{ "path": "../utils/tsconfig.build.json" }], "include": ["./src/**/*.ts", "./src/sqlite/*.ts"] diff --git a/packages/streams/src/BaseStream.ts b/packages/streams/src/BaseStream.ts index 9a5d47c7c..5b49bd62d 100644 --- a/packages/streams/src/BaseStream.ts +++ b/packages/streams/src/BaseStream.ts @@ -1,8 +1,9 @@ import { makePromiseKit } from '@endo/promise-kit'; import type { Reader, Writer } from '@endo/stream'; import { stringify } from '@ocap/utils'; +import type { PromiseCallbacks } from '@ocap/utils'; -import type { Dispatchable, PromiseCallbacks, Writable } from './utils.ts'; +import type { Dispatchable, Writable } from './utils.ts'; import { makeDoneResult, makePendingResult, diff --git a/packages/streams/src/utils.ts b/packages/streams/src/utils.ts index a5db15f72..b023d0fab 100644 --- a/packages/streams/src/utils.ts +++ b/packages/streams/src/utils.ts @@ -12,14 +12,7 @@ import { stringify } from '@ocap/utils'; export type { Reader, Writer }; -export type PromiseCallbacks = { - resolve: (value: unknown) => void; - reject: (reason: unknown) => void; -}; - export const StreamSentinel = { - // Not a problem if we don't use the word "Error" in this scope, which we won't. - Error: '@@StreamError', Done: '@@StreamDone', } as const; diff --git a/packages/test-utils/src/env/mock-kernel.ts b/packages/test-utils/src/env/mock-kernel.ts index 8b8c09cce..b7bd0e9e2 100644 --- a/packages/test-utils/src/env/mock-kernel.ts +++ b/packages/test-utils/src/env/mock-kernel.ts @@ -13,6 +13,7 @@ type ResetMocks = () => void; type SetMockBehavior = (options: { isVatConfig?: boolean; isVatId?: boolean; + isKernelCommand?: boolean; }) => void; export const setupOcapKernelMock = (): { @@ -21,14 +22,14 @@ export const setupOcapKernelMock = (): { } => { let isVatConfigMock = true; let isVatIdMock = true; - + let isKernelCommandMock = true; // Mock implementation vi.doMock('@ocap/kernel', () => { const VatIdStruct = define('VatId', () => isVatIdMock); const VatConfigStruct = define('VatConfig', () => isVatConfigMock); return { - isKernelCommand: () => true, + isKernelCommand: () => isKernelCommandMock, isVatId: () => isVatIdMock, isVatConfig: () => isVatConfigMock, VatIdStruct, @@ -63,10 +64,12 @@ export const setupOcapKernelMock = (): { resetMocks: (): void => { isVatConfigMock = true; isVatIdMock = true; + isKernelCommandMock = true; }, setMockBehavior: (options: { isVatConfig?: boolean; isVatId?: boolean; + isKernelCommand?: boolean; }): void => { if (typeof options.isVatConfig === 'boolean') { isVatConfigMock = options.isVatConfig; @@ -74,6 +77,9 @@ export const setupOcapKernelMock = (): { if (typeof options.isVatId === 'boolean') { isVatIdMock = options.isVatId; } + if (typeof options.isKernelCommand === 'boolean') { + isKernelCommandMock = options.isKernelCommand; + } }, }; }; diff --git a/packages/utils/package.json b/packages/utils/package.json index 182880002..8a5fb9c6a 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -43,6 +43,8 @@ }, "dependencies": { "@endo/captp": "^4.4.0", + "@endo/promise-kit": "^1.1.6", + "@metamask/superstruct": "^3.1.0", "@metamask/utils": "^11.3.0" }, "devDependencies": { @@ -51,8 +53,6 @@ "@metamask/eslint-config": "^14.0.0", "@metamask/eslint-config-nodejs": "^14.0.0", "@metamask/eslint-config-typescript": "^14.0.0", - "@metamask/superstruct": "^3.1.0", - "@ocap/cli": "workspace:^", "@ocap/errors": "workspace:^", "@ocap/test-utils": "workspace:^", "@ts-bridge/cli": "^0.6.2", diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 184d7dfd6..c007a403c 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -2,6 +2,11 @@ export type { Logger } from './logger.ts'; export { makeLogger } from './logger.ts'; export { delay, makeCounter } from './misc.ts'; export { stringify } from './stringify.ts'; -export type { TypeGuard, ExtractGuardType } from './types.ts'; -export { isPrimitive, isTypedArray, isTypedObject } from './types.ts'; +export type { ExtractGuardType, PromiseCallbacks, TypeGuard } from './types.ts'; +export { + EmptyJsonArray, + isPrimitive, + isTypedArray, + isTypedObject, +} from './types.ts'; export { fetchValidatedJson } from './fetchValidatedJson.ts'; diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index 72d47facc..f8aa579c7 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -1,5 +1,8 @@ import type { Primitive } from '@endo/captp'; -import { isObject } from '@metamask/utils'; +import type { PromiseKit } from '@endo/promise-kit'; +import type { Infer } from '@metamask/superstruct'; +import { array, empty } from '@metamask/superstruct'; +import { isObject, UnsafeJsonStruct } from '@metamask/utils'; export type TypeGuard = (value: unknown) => value is Type; @@ -20,6 +23,7 @@ const primitives = new Set([ 'null', 'undefined', ]); + export const isPrimitive = (value: unknown): value is Primitive => value === null || primitives.has(typeof value); @@ -34,3 +38,12 @@ export const isTypedObject = ( isValue: TypeGuard, ): value is { [Key in keyof object]: ValueType } => isObject(value) && !Object.values(value).some((val) => !isValue(val)); + +export type PromiseCallbacks = Omit< + PromiseKit, + 'promise' +>; + +export const EmptyJsonArray = empty(array(UnsafeJsonStruct)); + +export type EmptyJsonArray = Infer; diff --git a/vitest.config.ts b/vitest.config.ts index 2256ea60e..6d1ed65f7 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -82,16 +82,16 @@ export default defineConfig({ lines: 100, }, 'packages/extension/**': { - statements: 83.24, - functions: 84.1, - branches: 81.29, - lines: 83.11, + statements: 81.76, + functions: 83.85, + branches: 78.98, + lines: 81.78, }, 'packages/kernel/**': { - statements: 74.73, + statements: 74.7, functions: 69.32, branches: 59.86, - lines: 75.08, + lines: 75.05, }, 'packages/nodejs/**': { statements: 72.91, diff --git a/yarn.lock b/yarn.lock index 4f65821b9..a4ff57646 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1992,6 +1992,7 @@ __metadata: "@metamask/eslint-config": "npm:^14.0.0" "@metamask/eslint-config-nodejs": "npm:^14.0.0" "@metamask/eslint-config-typescript": "npm:^14.0.0" + "@metamask/json-rpc-engine": "npm:^10.0.3" "@metamask/snaps-utils": "npm:^8.3.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.3.0" @@ -2399,13 +2400,13 @@ __metadata: dependencies: "@arethetypeswrong/cli": "npm:^0.17.3" "@endo/captp": "npm:^4.4.0" + "@endo/promise-kit": "npm:^1.1.6" "@metamask/auto-changelog": "npm:^4.0.0" "@metamask/eslint-config": "npm:^14.0.0" "@metamask/eslint-config-nodejs": "npm:^14.0.0" "@metamask/eslint-config-typescript": "npm:^14.0.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.3.0" - "@ocap/cli": "workspace:^" "@ocap/errors": "workspace:^" "@ocap/test-utils": "workspace:^" "@ts-bridge/cli": "npm:^0.6.2"