diff --git a/packages/extension/src/ui/App.module.css b/packages/extension/src/ui/App.module.css index 3425ab4ad..82e7123d4 100644 --- a/packages/extension/src/ui/App.module.css +++ b/packages/extension/src/ui/App.module.css @@ -494,3 +494,9 @@ div + .sent { gap: var(--spacing-sm); justify-content: flex-end; } + +.configControls { + display: flex; + gap: var(--spacing-xs); + justify-content: space-between; +} diff --git a/packages/extension/src/ui/components/ConfigEditor.test.tsx b/packages/extension/src/ui/components/ConfigEditor.test.tsx index a36e8df1d..b85934e3e 100644 --- a/packages/extension/src/ui/components/ConfigEditor.test.tsx +++ b/packages/extension/src/ui/components/ConfigEditor.test.tsx @@ -10,12 +10,13 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { ConfigEditor } from './ConfigEditor.js'; import type { KernelStatus } from '../../kernel-integration/messages.js'; -import clusterConfig from '../../vats/default-cluster.json'; +import defaultClusterConfig from '../../vats/default-cluster.json'; +import minimalClusterConfig from '../../vats/minimal-cluster.json'; import { usePanelContext } from '../context/PanelContext.js'; import { useKernelActions } from '../hooks/useKernelActions.js'; const mockStatus = { - clusterConfig, + clusterConfig: defaultClusterConfig, vats: [], }; @@ -157,7 +158,7 @@ describe('ConfigEditor Component', () => { const newStatus: KernelStatus = { clusterConfig: { - ...clusterConfig, + ...defaultClusterConfig, bootstrap: 'updated-config', }, vats: [], @@ -176,4 +177,20 @@ describe('ConfigEditor Component', () => { ); }); }); + + it('renders the config template selector with default option selected', () => { + render(); + const selector = screen.getByTestId('config-select'); + expect(selector).toBeInTheDocument(); + expect(selector).toHaveValue('Default'); + }); + + it('updates textarea when selecting a different template', async () => { + render(); + const selector = screen.getByTestId('config-select'); + const textarea = screen.getByTestId('config-textarea'); + expect(textarea).toHaveValue(JSON.stringify(defaultClusterConfig, null, 2)); + await userEvent.selectOptions(selector, 'Minimal'); + expect(textarea).toHaveValue(JSON.stringify(minimalClusterConfig, null, 2)); + }); }); diff --git a/packages/extension/src/ui/components/ConfigEditor.tsx b/packages/extension/src/ui/components/ConfigEditor.tsx index 9103067c6..2bf2a6fda 100644 --- a/packages/extension/src/ui/components/ConfigEditor.tsx +++ b/packages/extension/src/ui/components/ConfigEditor.tsx @@ -2,10 +2,22 @@ import type { ClusterConfig } from '@ocap/kernel'; import { useCallback, useEffect, useMemo, useState } from 'react'; import type { KernelStatus } from '../../kernel-integration/messages.js'; +import defaultConfig from '../../vats/default-cluster.json'; +import minimalConfig from '../../vats/minimal-cluster.json'; import styles from '../App.module.css'; import { usePanelContext } from '../context/PanelContext.js'; import { useKernelActions } from '../hooks/useKernelActions.js'; +type ConfigEntry = { + name: string; + config: ClusterConfig; +}; + +const availableConfigs: ConfigEntry[] = [ + { name: 'Default', config: defaultConfig }, + { name: 'Minimal', config: minimalConfig }, +]; + /** * Component for editing the kernel cluster configuration. * @@ -45,9 +57,14 @@ export const ConfigEditorInner: React.FC<{ status: KernelStatus }> = ({ [config, updateClusterConfig], ); - if (!config) { - return null; - } + const handleSelectConfig = useCallback((configName: string) => { + const selectedConfig = availableConfigs.find( + (item) => item.name === configName, + )?.config; + if (selectedConfig) { + setConfig(JSON.stringify(selectedConfig, null, 2)); + } + }, []); return (
@@ -59,21 +76,38 @@ export const ConfigEditorInner: React.FC<{ status: KernelStatus }> = ({ className={styles.configTextarea} data-testid="config-textarea" /> -
- - + +
); diff --git a/packages/extension/src/vats/empty-vat.js b/packages/extension/src/vats/empty-vat.js new file mode 100644 index 000000000..380e34593 --- /dev/null +++ b/packages/extension/src/vats/empty-vat.js @@ -0,0 +1,19 @@ +import { Far } from '@endo/marshal'; + +/** + * Build function for simple test vat. + * + * @param {unknown} _vatPowers - Special powers granted to this vat (not used here). + * @param {unknown} parameters - Initialization parameters from the vat's config object. + * @param {unknown} _baggage - Root of vat's persistent state (not used here). + * @returns {unknown} The root object for the new vat. + */ +export function buildRootObject(_vatPowers, parameters, _baggage) { + const name = parameters?.name ?? 'anonymous'; + console.log(`buildRootObject "${name}"`); + return Far('root', { + bootstrap() { + console.log(`vat ${name} bootstrap() called`); + }, + }); +} diff --git a/packages/extension/src/vats/minimal-cluster.json b/packages/extension/src/vats/minimal-cluster.json new file mode 100644 index 000000000..5a210e028 --- /dev/null +++ b/packages/extension/src/vats/minimal-cluster.json @@ -0,0 +1,12 @@ +{ + "bootstrap": "main", + "forceReset": true, + "vats": { + "main": { + "bundleSpec": "http://localhost:3000/empty-vat.bundle", + "parameters": { + "name": "EmptyVat" + } + } + } +} diff --git a/packages/extension/test/e2e/vat-manager.test.ts b/packages/extension/test/e2e/vat-manager.test.ts index 4e33f2fec..165b1fb55 100644 --- a/packages/extension/test/e2e/vat-manager.test.ts +++ b/packages/extension/test/e2e/vat-manager.test.ts @@ -1,7 +1,8 @@ import { test, expect } from '@playwright/test'; import type { Page, BrowserContext } from '@playwright/test'; -import clusterConfig from '../../src/vats/default-cluster.json' assert { type: 'json' }; +import defaultClusterConfig from '../../src/vats/default-cluster.json' assert { type: 'json' }; +import minimalClusterConfig from '../../src/vats/minimal-cluster.json' assert { type: 'json' }; import { makeLoadExtension } from '../helpers/extension'; test.describe('Vat Manager', () => { @@ -147,10 +148,10 @@ test.describe('Vat Manager', () => { const vatTable = popupPage.locator('[data-testid="vat-table"]'); await expect(vatTable).toBeVisible(); await expect(vatTable.locator('tr')).toHaveCount( - Object.keys(clusterConfig.vats).length + 1, // +1 for header row + Object.keys(defaultClusterConfig.vats).length + 1, // +1 for header row ); // Verify each default vat is present in the table - for (const [, vatConfig] of Object.entries(clusterConfig.vats)) { + for (const [, vatConfig] of Object.entries(defaultClusterConfig.vats)) { await expect(vatTable).toContainText(vatConfig.parameters.name); await expect(vatTable).toContainText(vatConfig.bundleSpec); } @@ -161,7 +162,7 @@ test.describe('Vat Manager', () => { const configTextarea = popupPage.locator('[data-testid="config-textarea"]'); await expect(configTextarea).toBeVisible(); await expect(configTextarea).toHaveValue( - JSON.stringify(clusterConfig, null, 2), + JSON.stringify(defaultClusterConfig, null, 2), ); // Test invalid JSON handling await configTextarea.fill('{ invalid json }'); @@ -170,12 +171,13 @@ test.describe('Vat Manager', () => { // Verify original vats still exist const vatTable = popupPage.locator('[data-testid="vat-table"]'); const firstVatKey = Object.keys( - clusterConfig.vats, - )[0] as keyof typeof clusterConfig.vats; - const originalVatName = clusterConfig.vats[firstVatKey].parameters.name; + defaultClusterConfig.vats, + )[0] as keyof typeof defaultClusterConfig.vats; + const originalVatName = + defaultClusterConfig.vats[firstVatKey].parameters.name; await expect(vatTable).toContainText(originalVatName); // Modify config with new vat name - const modifiedConfig = structuredClone(clusterConfig); + const modifiedConfig = structuredClone(defaultClusterConfig); modifiedConfig.vats[firstVatKey].parameters.name = 'SuperAlice'; // Update config and reload await configTextarea.fill(JSON.stringify(modifiedConfig, null, 2)); @@ -208,4 +210,27 @@ test.describe('Vat Manager', () => { ) .toBeTruthy(); }); + + test('should handle config template selection', async () => { + // Get initial config textarea content + const configTextarea = popupPage.locator('[data-testid="config-textarea"]'); + await expect(configTextarea).toBeVisible(); + const initialConfig = await configTextarea.inputValue(); + // Select minimal config template + const configSelect = popupPage.locator('[data-testid="config-select"]'); + await configSelect.selectOption('Minimal'); + // Verify config textarea was updated with minimal config + const minimalConfig = await configTextarea.inputValue(); + expect(minimalConfig).not.toBe(initialConfig); + expect(JSON.parse(minimalConfig)).toMatchObject(minimalClusterConfig); + // Update and reload with minimal config + await popupPage.click('button:text("Update and Reload")'); + // Verify vat table shows only the main vat + const vatTable = popupPage.locator('[data-testid="vat-table"]'); + await expect(vatTable).toBeVisible(); + await expect(vatTable.locator('tr')).toHaveCount(2); // Header + 1 row + await expect(vatTable).toContainText( + minimalClusterConfig.vats.main.parameters.name, + ); + }); }); diff --git a/vitest.config.ts b/vitest.config.ts index 3bf92da73..99d446348 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -68,10 +68,10 @@ export default defineConfig({ lines: 100, }, 'packages/extension/**': { - statements: 73.63, - functions: 78.97, + statements: 74.07, + functions: 79.39, branches: 70.86, - lines: 73.67, + lines: 74.11, }, 'packages/kernel/**': { statements: 48.54,