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
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import '@ocap/test-utils/mock-endoify';
import { define, literal, object } from '@metamask/superstruct';
import type {
Kernel,
KernelCommand,
VatId,
VatConfig,
KVStore,
} from '@ocap/kernel';
import { setupOcapKernelMock } from '@ocap/test-utils';
import { describe, it, expect, vi, beforeEach } from 'vitest';

import type { KernelControlCommand } from './messages.js';
Expand All @@ -19,35 +19,16 @@ vi.mock('@ocap/utils', () => ({
}),
}));

let isVatConfigMock = true;
let isVatIdMock = true;

// Mock kernel validation functions
// because vitest needs to extend Error stack and under SES it fails
vi.mock('@ocap/kernel', () => ({
isKernelCommand: () => true,
isVatId: () => isVatIdMock,
isVatConfig: () => isVatConfigMock,
VatIdStruct: define<VatId>('VatId', () => isVatIdMock),
VatConfigStruct: define<VatConfig>('VatConfig', () => isVatConfigMock),
KernelSendVatCommandStruct: object({
id: literal('v0'),
payload: object({
method: literal('ping'),
params: literal(null),
}),
}),
}));
const { setMockBehavior, resetMocks } = setupOcapKernelMock();

describe('handlePanelMessage', () => {
let mockKernel: Kernel;
let mockKVStore: KVStore;

beforeEach(() => {
vi.resetModules();
resetMocks();

isVatConfigMock = true;
isVatIdMock = true;
mockKVStore = {
get: vi.fn(),
getRequired: vi.fn(),
Expand Down Expand Up @@ -117,7 +98,7 @@ describe('handlePanelMessage', () => {

it('should handle invalid vat configuration', async () => {
const { handlePanelMessage } = await import('./handle-panel-message');
isVatConfigMock = false;
setMockBehavior({ isVatConfig: false });

const message: KernelControlCommand = {
id: 'test-2',
Expand Down Expand Up @@ -173,7 +154,7 @@ describe('handlePanelMessage', () => {

it('should handle invalid vat ID for restartVat command', async () => {
const { handlePanelMessage } = await import('./handle-panel-message');
isVatIdMock = false;
setMockBehavior({ isVatId: false });

const message: KernelControlCommand = {
id: 'test-4',
Expand Down Expand Up @@ -277,6 +258,7 @@ describe('handlePanelMessage', () => {
payload: {
method: 'getStatus',
params: {
clusterConfig: undefined,
vats: [
{
id: 'v0',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Kernel, KVStore } from '@ocap/kernel';
import { describe, it, expect, vi } from 'vitest';

import { getStatusHandler } from './get-status.js';
import clusterConfig from '../../vats/default-cluster.json';

describe('getStatusHandler', () => {
const mockVats = [
Expand All @@ -11,6 +12,7 @@ describe('getStatusHandler', () => {
];

const mockKernel = {
clusterConfig,
getVats: vi.fn(() => mockVats),
} as unknown as Kernel;

Expand All @@ -24,13 +26,13 @@ describe('getStatusHandler', () => {
expect(getStatusHandler.schema).toBeDefined();
});

it('should return vats status', async () => {
it('should return vats status and cluster config', async () => {
const result = await getStatusHandler.implementation(
mockKernel,
mockKVStore,
null,
);
expect(mockKernel.getVats).toHaveBeenCalledOnce();
expect(result).toStrictEqual({ vats: mockVats });
expect(result).toStrictEqual({ vats: mockVats, clusterConfig });
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const getStatusHandler: CommandHandler<GetStatusMethod> = {
implementation: async (kernel: Kernel): Promise<Json> => {
return {
vats: kernel.getVats(),
};
clusterConfig: kernel.clusterConfig,
} as Json;
},
};
2 changes: 2 additions & 0 deletions packages/extension/src/kernel-integration/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { restartVatHandler } from './restart-vat.js';
import { sendVatCommandHandler } from './send-vat-command.js';
import { terminateAllVatsHandler } from './terminate-all-vats.js';
import { terminateVatHandler } from './terminate-vat.js';
import { updateClusterConfigHandler } from './update-cluster-config.js';

export const handlers = [
getStatusHandler,
Expand All @@ -18,4 +19,5 @@ export const handlers = [
restartVatHandler,
terminateVatHandler,
terminateAllVatsHandler,
updateClusterConfigHandler,
] as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import '@ocap/test-utils/mock-endoify';
import type { Kernel, KVStore } from '@ocap/kernel';
import { describe, it, expect, vi, beforeEach } from 'vitest';

import { reloadConfigHandler } from './reload-config.js';

describe('reloadConfigHandler', () => {
const mockKernel = {
reload: vi.fn().mockResolvedValue(undefined),
} as Partial<Kernel>;

const mockKVStore = {} as KVStore;

beforeEach(() => {
vi.clearAllMocks();
});

it('should call kernel.reload() and return null', async () => {
const result = await reloadConfigHandler.implementation(
mockKernel as Kernel,
mockKVStore,
null,
);

expect(mockKernel.reload).toHaveBeenCalledTimes(1);
expect(result).toBeNull();
});

it('should use the correct method name', () => {
expect(reloadConfigHandler.method).toBe('reload');
});

it('should use the clearState schema for params', () => {
expect(reloadConfigHandler.schema).toBeDefined();
});

it('should propagate errors from kernel.reload()', async () => {
const error = new Error('Reload failed');
vi.mocked(mockKernel.reload)?.mockRejectedValueOnce(error);

await expect(
reloadConfigHandler.implementation(
mockKernel as Kernel,
mockKVStore,
null,
),
).rejects.toThrow(error);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const reloadConfigHandler: CommandHandler<ReloadMethod> = {
method: KernelControlMethod.reload,
schema: KernelCommandPayloadStructs.clearState.schema.params,
implementation: async (kernel: Kernel): Promise<Json> => {
await kernel.reset();
await kernel.reload();
return null;
},
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { assert } from '@metamask/superstruct';
import type { Json } from '@metamask/utils';
import { isKernelCommand, KernelSendVatCommandStruct } from '@ocap/kernel';
import { isKernelCommand } from '@ocap/kernel';
import type { Kernel, KVStore } from '@ocap/kernel';

import type { CommandHandler, CommandParams } from '../command-registry.js';
Expand All @@ -27,7 +26,6 @@ export const sendVatCommandHandler: CommandHandler<SendVatCommandMethod> = {
throw new Error('Vat ID required for this command');
}

assert(params, KernelSendVatCommandStruct);
const result = await kernel.sendVatCommand(params.id, params.payload);
return { result };
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import '@ocap/test-utils/mock-endoify';
import type { Kernel, KVStore } from '@ocap/kernel';
import { describe, it, expect } from 'vitest';

import { updateClusterConfigHandler } from './update-cluster-config.js';

describe('updateClusterConfigHandler', () => {
const mockKernel = {
clusterConfig: null,
} as Partial<Kernel>;

const mockKvStore = {} as KVStore;

const testConfig = {
bootstrap: 'testVat',
vats: {
testVat: {
sourceSpec: 'test-source',
},
},
};

it('should update kernel cluster config', async () => {
const result = await updateClusterConfigHandler.implementation(
mockKernel as Kernel,
mockKvStore,
{ config: testConfig },
);

expect(mockKernel.clusterConfig).toStrictEqual(testConfig);
expect(result).toBeNull();
});

it('should use the correct method name', () => {
expect(updateClusterConfigHandler.method).toBe('updateClusterConfig');
});

it('should validate the config using the correct schema', () => {
expect(updateClusterConfigHandler.schema).toBeDefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { Json } from '@metamask/utils';
import type { ClusterConfig, Kernel, KVStore } from '@ocap/kernel';

import type { CommandHandler } from '../command-registry.js';
import {
KernelCommandPayloadStructs,
KernelControlMethod,
} from '../messages.js';

export const updateClusterConfigHandler: CommandHandler<
typeof KernelControlMethod.updateClusterConfig
> = {
method: KernelControlMethod.updateClusterConfig,
schema: KernelCommandPayloadStructs.updateClusterConfig.schema.params,
implementation: async (
kernel: Kernel,
_kvStore: KVStore,
params: { config: ClusterConfig },
): Promise<Json> => {
kernel.clusterConfig = params.config;
return null;
},
};
47 changes: 15 additions & 32 deletions packages/extension/src/kernel-integration/kernel-worker.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,18 @@
import type {
ClusterConfig,
KernelCommand,
KernelCommandReply,
ClusterConfig,
} from '@ocap/kernel';
import { isKernelCommand, Kernel } from '@ocap/kernel';
import { ClusterConfigStruct, isKernelCommand, Kernel } from '@ocap/kernel';
import type { PostMessageTarget } from '@ocap/streams';
import { MessagePortDuplexStream, receiveMessagePort } from '@ocap/streams';
import { makeLogger } from '@ocap/utils';
import { fetchValidatedJson, makeLogger } from '@ocap/utils';

import { handlePanelMessage } from './handle-panel-message.js';
import { makeSQLKVStore } from './sqlite-kv-store.js';
import { receiveUiConnections } from './ui-connections.js';
import { ExtensionVatWorkerClient } from './VatWorkerClient.js';

const bundleHost = 'http://localhost:3000'; // XXX placeholder
const sampleBundle = 'sample-vat.bundle';
const bundleURL = `${bundleHost}/${sampleBundle}`;

const defaultSubcluster: ClusterConfig = {
bootstrap: 'alice',
vats: {
alice: {
bundleSpec: bundleURL,
parameters: {
name: 'Alice',
},
},
bob: {
bundleSpec: bundleURL,
parameters: {
name: 'Bob',
},
},
carol: {
bundleSpec: bundleURL,
parameters: {
name: 'Carol',
},
},
},
};

const logger = makeLogger('[kernel worker]');

main().catch(logger.error);
Expand All @@ -65,13 +37,24 @@ async function main(): Promise<void> {
);
const kvStore = await makeSQLKVStore();

const kernel = new Kernel(kernelStream, vatWorkerClient, kvStore);
const kernel = new Kernel(kernelStream, vatWorkerClient, kvStore, {
// XXX Warning: Clearing storage here is a hack to aid development
// debugging, wherein extension reloads are almost exclusively used for
// retrying after tweaking some fix. The following line will prevent
// the accumulation of long term kernel state.
resetStorage: true,
});
receiveUiConnections(
async (message) => handlePanelMessage(kernel, kvStore, message),
logger,
);
await kernel.init();

const defaultSubcluster = await fetchValidatedJson<ClusterConfig>(
new URL('../vats/default-cluster.json', import.meta.url).href,
ClusterConfigStruct,
);

await Promise.all([
vatWorkerClient.start(),
// XXX We are mildly concerned that there's a small chance that a race here
Expand Down
Loading