feat(tools): Enhance guest tools installation and management
- Added new API endpoints for tool catalog and installation management. - Updated usage documentation to include new tool installation features. - Implemented asynchronous tool installation with job tracking. - Introduced integration tests for tool installation in disposable VMs. - Enhanced dashboard UI to reflect tool installation status and usage instructions. - Improved error handling and logging for tool installation processes. - Added support for multiple Python versions and Conda environments. - Refactored tool management logic to ensure better state handling and job persistence.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
// Optional browser integration: real API/job persistence, simulated guest commands.
|
||||
const assert = require('node:assert/strict');
|
||||
const { spawn } = require('node:child_process');
|
||||
const { mkdtemp, rm } = require('node:fs/promises');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
(async () => {
|
||||
const temporary = await mkdtemp(path.join(os.tmpdir(), 'nest-tool-browser-'));
|
||||
const port = process.env.NEST_BROWSER_TEST_PORT || '18766';
|
||||
const origin = `http://127.0.0.1:${port}`;
|
||||
const server = spawn(process.env.NEST_TEST_PYTHON || 'python3',
|
||||
['-B', '-m', 'tests.browser_tool_server', '--base', temporary, '--port', port],
|
||||
{ cwd: path.resolve(__dirname, '..'), stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
let logs = '';
|
||||
server.stdout.on('data', (data) => { logs = (logs + data).slice(-16000); });
|
||||
server.stderr.on('data', (data) => { logs = (logs + data).slice(-16000); });
|
||||
let browser;
|
||||
let page;
|
||||
try {
|
||||
let ready = false;
|
||||
for (let attempt = 0; attempt < 100; attempt++) {
|
||||
if (server.exitCode !== null) throw new Error(`Fixture server exited: ${logs}`);
|
||||
try { if ((await fetch(`${origin}/health`)).ok) { ready = true; break; } } catch {}
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
assert.ok(ready, logs);
|
||||
browser = await chromium.launch({ headless: true });
|
||||
page = await browser.newPage({ viewport: { width: 1440, height: 1100 } });
|
||||
const errors = [];
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
const installed = (key) => page.waitForFunction((value) =>
|
||||
document.querySelector(`[data-install-tool="${value}"]`)?.textContent === 'Installed', key);
|
||||
await page.goto(`${origin}/dashboard#create`);
|
||||
await page.locator('#token-input').fill('browser-test-token');
|
||||
await page.locator('#auth-btn').click();
|
||||
await page.locator('[data-create-tool="python-3.12"]').check();
|
||||
await page.locator('[data-create-tool="git"]').check();
|
||||
await page.locator('[data-create-tool="nodejs"]').check();
|
||||
assert.equal(await page.locator('[data-create-tool]').count(), 12);
|
||||
assert.match(await page.locator('#create-summary-details').textContent(), /Git, Node.js, Python 3.12/);
|
||||
const layout = await page.evaluate(() => ({
|
||||
parent: document.querySelector('.create-tools-section').parentElement.className,
|
||||
children: document.querySelector('#create-form').children.length,
|
||||
}));
|
||||
assert.equal(layout.parent, 'create-fields');
|
||||
assert.equal(layout.children, 2);
|
||||
await page.locator('#create-name').fill('tool-workspace');
|
||||
await page.locator('#create-submit').click();
|
||||
await page.waitForURL((url) => /#vm\/vm-[a-f0-9]+\/tools$/.test(url.hash));
|
||||
const createdUrl = page.url();
|
||||
await page.locator('#tools-job').waitFor({ state: 'visible' });
|
||||
// A reload can occur while the server-owned worker is still installing.
|
||||
await page.reload();
|
||||
await page.waitForFunction(() => document.querySelector('#tools-job-title').textContent === 'Complete');
|
||||
await installed('curl');
|
||||
assert.equal(await page.locator('.tool-installed').count(), 4); // Includes image-provided curl.
|
||||
assert.ok(await page.locator('[data-install-tool="python-3.12"]').isDisabled());
|
||||
await page.locator('#tools-job-items details').first().locator('summary').click();
|
||||
assert.match(await page.locator('#tools-job-items').textContent(), /Verified and installed git/);
|
||||
await page.locator('[data-install-tool="php"]').click();
|
||||
await page.waitForFunction(() => document.querySelector('#tools-job-title').textContent === 'Failed');
|
||||
await page.locator('#tools-job-items details[data-output="php"] summary').click();
|
||||
assert.match(await page.locator('.tool-output').textContent(), /<script>bad/);
|
||||
assert.equal(await page.locator('#tools-job-items script').count(), 0);
|
||||
await page.locator('#tools-retry').click();
|
||||
await page.waitForFunction(() => document.querySelector('#tools-job-title').textContent === 'Complete');
|
||||
await installed('php');
|
||||
assert.ok(await page.locator('[data-install-tool="php"]').isDisabled());
|
||||
assert.equal(await page.locator('#tools-job-select option').count(), 3);
|
||||
|
||||
// The same click boots a stopped VM before installing its selected tool.
|
||||
await page.goto(`${origin}/dashboard#vm/vm-other/tools`);
|
||||
await page.locator('[data-install-tool="htop"]').waitFor();
|
||||
assert.equal(await page.locator('[data-install-tool="htop"]').textContent(), 'Start & install');
|
||||
await page.locator('[data-install-tool="htop"]').click();
|
||||
await installed('htop');
|
||||
await page.waitForFunction(() => document.querySelector('#tools-job-title').textContent === 'Complete');
|
||||
assert.ok(await page.locator('[data-install-tool="htop"]').isDisabled());
|
||||
assert.ok(!(await page.locator('#tools-job-items').textContent()).includes('python-3.12'));
|
||||
|
||||
await page.goto(createdUrl);
|
||||
await page.locator('[data-tool-select="wget"]').check();
|
||||
await page.locator('[data-tool-select="micromamba"]').check();
|
||||
await page.locator('#tools-install-selected').click();
|
||||
await installed('wget');
|
||||
await installed('micromamba');
|
||||
await page.waitForFunction(() => document.querySelector('#tools-job-title').textContent === 'Complete');
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.waitForTimeout(250);
|
||||
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth), 'mobile layout overflows');
|
||||
if (process.env.NEST_BROWSER_SCREENSHOT) await page.screenshot({ path: process.env.NEST_BROWSER_SCREENSHOT, fullPage: true });
|
||||
assert.deepEqual(errors, []);
|
||||
console.log('Browser tools passed: creation, background/reload, detection, failure/retry, safe output, history, Start & install, VM isolation, multi-select, and mobile layout.');
|
||||
} catch (error) {
|
||||
if (page) console.error('Tool UI:', await page.locator('#tools-error').textContent(), await page.locator('#tools-job-message').textContent());
|
||||
console.error(logs);
|
||||
throw error;
|
||||
} finally {
|
||||
await browser?.close();
|
||||
server.kill('SIGTERM');
|
||||
if (server.exitCode === null) await new Promise((resolve) => server.once('exit', resolve));
|
||||
await rm(temporary, { recursive: true, force: true });
|
||||
}
|
||||
})().catch((error) => { console.error(error); process.exitCode = 1; });
|
||||
Reference in New Issue
Block a user