feat: add session rename and delete functionality with validation
- Introduced `SessionRename` model for renaming sessions with validation on name length and content. - Implemented `rename_session` endpoint in `session_routes.py` to handle session renaming. - Added `delete_session` endpoint to remove sessions, ensuring proper cleanup of associated data. - Enhanced `SessionStore` with `rename` and `delete` methods, including validation and transaction management. - Updated `SessionManager` to manage session renaming and deletion, ensuring live terminal state is preserved. - Added tests for renaming and deleting sessions, ensuring proper functionality and validation. - Introduced a new `tool_catalog.py` for managing guest tool selections and installation recipes.
This commit is contained in:
@@ -30,7 +30,8 @@ const { chromium } = require('playwright');
|
||||
}
|
||||
assert.ok(ready, `Fixture server did not start: ${logs}`);
|
||||
browser = await chromium.launch({ headless: true });
|
||||
page = await browser.newPage({ viewport: { width: 1440, height: 1100 } });
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 1100 } });
|
||||
page = await context.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
const connected = () => page.waitForFunction(() => document.querySelector('#session-status').textContent === 'Connected');
|
||||
@@ -51,15 +52,49 @@ const { chromium } = require('playwright');
|
||||
await output('result:echo alpha');
|
||||
assert.equal(await page.locator('.session-item').count(), 1);
|
||||
|
||||
// Rename is cancellable, validated, persisted, and preserves the live widget.
|
||||
const firstId = new URL(firstUrl).hash.split('/').at(-1);
|
||||
const firstRow = () => page.locator(`.session-item[data-session-id="${firstId}"]`);
|
||||
await page.locator('#session-rename').click();
|
||||
await page.locator('#session-name-input').fill('Discarded name');
|
||||
await page.locator('#session-rename-cancel').click();
|
||||
assert.equal(await firstRow().locator('strong').textContent(), 'Session 1');
|
||||
await page.evaluate(() => { window.originalTerminal = document.querySelector('#session-terminal .xterm'); });
|
||||
await page.locator('#session-rename').click();
|
||||
await page.locator('#session-name-input').fill(' ');
|
||||
await page.locator('#session-rename-save').click();
|
||||
assert.ok(await page.locator('#session-rename-dialog').isVisible());
|
||||
assert.match(await page.locator('#session-rename-error').textContent(), /Enter a session name/);
|
||||
await page.locator('#session-name-input').fill(' Deployment ');
|
||||
await page.locator('#session-rename-save').click();
|
||||
await page.locator('#session-rename-dialog').waitFor({ state: 'hidden' });
|
||||
await page.waitForFunction(() => document.querySelector('#session-title').textContent.startsWith('Deployment'));
|
||||
assert.equal(page.url(), firstUrl);
|
||||
assert.ok(await page.evaluate(() => window.originalTerminal === document.querySelector('#session-terminal .xterm')));
|
||||
await page.reload();
|
||||
await connected();
|
||||
await output('result:echo alpha');
|
||||
assert.equal(await firstRow().locator('strong').textContent(), 'Deployment');
|
||||
|
||||
await page.locator('#detail-actions [data-action="launch-session"]').click();
|
||||
await page.waitForURL((url) => url.href !== firstUrl);
|
||||
await connected();
|
||||
const secondUrl = page.url();
|
||||
assert.equal(await page.locator('.session-item').count(), 2);
|
||||
assert.ok(!(await page.locator('#session-terminal').textContent()).includes('result:echo alpha'));
|
||||
await page.keyboard.type('echo beta');
|
||||
await page.keyboard.press('Enter');
|
||||
await output('result:echo beta');
|
||||
await page.locator('.session-item').filter({ hasText: 'Session 1' }).click();
|
||||
// History actions also work without selecting the affected session.
|
||||
await firstRow().locator('[data-session-action="rename"]').click();
|
||||
await page.locator('#session-name-input').fill('Operations <alpha>');
|
||||
await page.locator('#session-rename-save').click();
|
||||
await page.locator('#session-rename-dialog').waitFor({ state: 'hidden' });
|
||||
assert.equal(page.url(), secondUrl);
|
||||
assert.equal(await firstRow().locator('strong').textContent(), 'Operations <alpha>');
|
||||
assert.equal(await firstRow().locator('alpha').count(), 0);
|
||||
await output('result:echo beta');
|
||||
await firstRow().locator('.session-link').click();
|
||||
await connected();
|
||||
await output('result:echo alpha');
|
||||
assert.ok(!(await page.locator('#session-terminal').textContent()).includes('result:echo beta'));
|
||||
@@ -80,14 +115,59 @@ const { chromium } = require('playwright');
|
||||
await page.reload();
|
||||
await output('result:echo alpha');
|
||||
assert.ok(await page.locator('#session-end').isDisabled());
|
||||
await page.locator('#session-rename').click();
|
||||
await page.locator('#session-name-input').fill('Archived run');
|
||||
await page.locator('#session-rename-save').click();
|
||||
await page.locator('#session-rename-dialog').waitFor({ state: 'hidden' });
|
||||
await page.reload();
|
||||
await output('result:echo alpha');
|
||||
assert.equal(await firstRow().locator('strong').textContent(), 'Archived run');
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.waitForTimeout(300);
|
||||
const fits = await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth);
|
||||
assert.ok(fits, 'mobile layout overflows the viewport');
|
||||
assert.deepEqual(errors, []);
|
||||
if (process.env.NEST_BROWSER_SCREENSHOT) await page.screenshot({ path: process.env.NEST_BROWSER_SCREENSHOT, fullPage: true });
|
||||
console.log('Browser sessions passed: launch, terminal input, reload/reconnect, multiple sessions, VM isolation, closed history, and mobile layout.');
|
||||
|
||||
// Cancel preserves history; confirmation removes an ended session and clears selection.
|
||||
await page.locator('#session-delete').click();
|
||||
await page.locator('#confirm-dialog button[value="cancel"]').click();
|
||||
assert.equal(await page.locator('.session-item').count(), 2);
|
||||
await firstRow().locator('[data-session-action="delete"]').click();
|
||||
await page.locator('#confirm-ok').click();
|
||||
await page.waitForURL((url) => url.hash === '#vm/vm-test/sessions');
|
||||
await page.waitForFunction(() => document.querySelector('#vm-session-count').textContent === '1');
|
||||
assert.ok(await page.locator('#session-terminal').isHidden());
|
||||
await page.reload();
|
||||
await page.waitForFunction(() => document.querySelector('#vm-session-count').textContent === '1');
|
||||
assert.equal(await firstRow().count(), 0);
|
||||
|
||||
// Deleting an active session clears other attached browser views as well.
|
||||
await page.goto(secondUrl);
|
||||
await connected();
|
||||
await output('result:echo beta');
|
||||
const observer = await page.context().newPage();
|
||||
observer.on('pageerror', (error) => errors.push(error.message));
|
||||
await observer.goto(secondUrl);
|
||||
await observer.locator('#token-input').fill('browser-test-token');
|
||||
await observer.locator('#auth-btn').click();
|
||||
await observer.waitForFunction(() => document.querySelector('#session-status').textContent === 'Connected');
|
||||
await page.locator('#session-rename').click();
|
||||
await page.locator('#session-name-input').fill('Second workspace');
|
||||
await page.locator('#session-rename-save').click();
|
||||
await page.locator('#session-rename-dialog').waitFor({ state: 'hidden' });
|
||||
await observer.waitForFunction(() => document.querySelector('#session-title').textContent.startsWith('Second workspace'));
|
||||
await page.locator('#session-delete').click();
|
||||
await page.locator('#confirm-ok').click();
|
||||
await page.waitForFunction(() => document.querySelector('#vm-session-count').textContent === '0');
|
||||
await observer.waitForURL((url) => url.hash === '#vm/vm-test/sessions');
|
||||
assert.ok(await observer.locator('#session-terminal').isHidden());
|
||||
await page.reload();
|
||||
await page.waitForFunction(() => document.querySelector('#vm-session-count').textContent === '0');
|
||||
assert.equal(await page.locator('.session-item').count(), 0);
|
||||
assert.deepEqual(errors, []);
|
||||
await observer.close();
|
||||
console.log('Browser sessions passed: terminal/reconnect, VM isolation, persistent rename, active/ended deletion, cancellation, live browser updates, and mobile layout.');
|
||||
} catch (error) {
|
||||
if (page) console.error('Browser state:', await page.locator('#sessions-error').textContent(), await page.locator('#session-status').textContent());
|
||||
console.error(logs);
|
||||
|
||||
Reference in New Issue
Block a user