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,