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
6 changes: 6 additions & 0 deletions packages/extension/src/ui/App.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
23 changes: 20 additions & 3 deletions packages/extension/src/ui/components/ConfigEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
};

Expand Down Expand Up @@ -157,7 +158,7 @@ describe('ConfigEditor Component', () => {

const newStatus: KernelStatus = {
clusterConfig: {
...clusterConfig,
...defaultClusterConfig,
bootstrap: 'updated-config',
},
vats: [],
Expand All @@ -176,4 +177,20 @@ describe('ConfigEditor Component', () => {
);
});
});

it('renders the config template selector with default option selected', () => {
render(<ConfigEditor />);
const selector = screen.getByTestId('config-select');
expect(selector).toBeInTheDocument();
expect(selector).toHaveValue('Default');
});

it('updates textarea when selecting a different template', async () => {
render(<ConfigEditor />);
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));
});
});
68 changes: 51 additions & 17 deletions packages/extension/src/ui/components/ConfigEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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 (
<div className={styles.configEditor}>
Expand All @@ -59,21 +76,38 @@ export const ConfigEditorInner: React.FC<{ status: KernelStatus }> = ({
className={styles.configTextarea}
data-testid="config-textarea"
/>
<div className={styles.configEditorButtons}>
<button
onClick={() => handleUpdate(false)}
className={styles.buttonPrimary}
data-testid="update-config"
>
Update Config
</button>
<button
onClick={() => handleUpdate(true)}
className={styles.buttonBlack}
data-testid="update-and-restart"
<div className={styles.configControls}>
<select
className={styles.select}
onChange={(event) => handleSelectConfig(event.target.value)}
defaultValue={availableConfigs[0]?.name}
data-testid="config-select"
>
Update and Reload
</button>
<option value="" disabled>
Select template...
</option>
{availableConfigs.map(({ name }) => (
<option key={name} value={name}>
{name}
</option>
))}
</select>
<div className={styles.configEditorButtons}>
<button
onClick={() => handleUpdate(false)}
className={styles.buttonPrimary}
data-testid="update-config"
>
Update Config
</button>
<button
onClick={() => handleUpdate(true)}
className={styles.buttonBlack}
data-testid="update-and-restart"
>
Update and Reload
</button>
</div>
</div>
</div>
);
Expand Down
19 changes: 19 additions & 0 deletions packages/extension/src/vats/empty-vat.js
Original file line number Diff line number Diff line change
@@ -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`);
},
});
}
12 changes: 12 additions & 0 deletions packages/extension/src/vats/minimal-cluster.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"bootstrap": "main",
"forceReset": true,
"vats": {
"main": {
"bundleSpec": "http://localhost:3000/empty-vat.bundle",
"parameters": {
"name": "EmptyVat"
}
}
}
}
41 changes: 33 additions & 8 deletions packages/extension/test/e2e/vat-manager.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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);
}
Expand All @@ -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 }');
Expand All @@ -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));
Expand Down Expand Up @@ -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,
);
});
});
6 changes: 3 additions & 3 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down