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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions packages/extension/src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ async function main(): Promise<void> {
value: async () =>
sendClusterCommand({
method: KernelCommandMethod.ping,
params: null,
params: [],
}),
},
sendMessage: {
Expand All @@ -55,7 +55,7 @@ async function main(): Promise<void> {
chrome.action.onClicked.addListener(() => {
sendClusterCommand({
method: KernelCommandMethod.ping,
params: null,
params: [],
}).catch(console.error);
});

Expand Down
275 changes: 275 additions & 0 deletions packages/extension/src/kernel-integration/command-registry.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof handler.method>);
});
});

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');
});
});
});
40 changes: 2 additions & 38 deletions packages/extension/src/kernel-integration/command-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,6 @@ export type CommandHandler<Method extends KernelControlMethod> = {
) => Promise<Json>;
};

export type Middleware = (
next: (
kernel: Kernel,
kernelDatabase: KernelDatabase,
params: unknown,
) => Promise<Json>,
) => (
kernel: Kernel,
kernelDatabase: KernelDatabase,
params: unknown,
) => Promise<Json>;

/**
* A registry for kernel commands.
*/
Expand All @@ -57,8 +45,6 @@ export class KernelCommandRegistry {
CommandHandler<KernelControlMethod>
>();

readonly #middlewares: Middleware[] = [];

/**
* Register a command handler.
*
Expand All @@ -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.
*
Expand All @@ -101,20 +78,7 @@ export class KernelCommandRegistry {
throw new Error(`Unknown method: ${method}`);
}

let chain = async (
k: Kernel,
kdb: KernelDatabase,
param: unknown,
): Promise<Json> => {
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);
}
}
Loading