mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-08 06:28:33 +00:00
* feat(workspaces): implement active workspace restoration from snapshot - Added a new action to restore the active workspace from a snapshot. - Integrated workspace restoration into IPC events for improved user experience. - Refactored workspace loading logic to prioritize valid workspaces and handle invalid paths. - Introduced utility functions for workspace path validation and normalization. - Added tests to ensure correct functionality of workspace restoration and validation logic. * feat(snapshot): refactor snapshot serialization and add environment preservation logic - Moved snapshot serialization logic to a new file for better organization. - Implemented environment preservation for collections during snapshot serialization. - Added utility functions to handle collection environment checks and serialization. - Introduced tests to validate the new serialization behavior and environment preservation. - Enhanced existing tests to cover edge cases for unmounted collections and environment persistence. * Add tests for missing and invalid environment files * feat(snapshot): refactor devTools normalization and add saveToken to IPC handler * refactor(snapshot): remove saveToken from IPC handler for renderer:snapshot:save * feat(snapshot): implement collection deduplication logic in SnapshotManager and add corresponding tests * feat(snapshot): enhance workspace-specific environment handling for shared collections * feat(snapshot): remove sourceWorkspacePathname from snapshot serialization and related tests * feat(snapshot): remove sourceWorkspacePathname from SnapshotManager and add tests for shared collection environment restoration * feat(snapshot): refactor collection deduplication tests and enhance snapshot seeding utility * feat(snapshot): enhance workspace path normalization and improve snapshot environment tests --------- Co-authored-by: engineering-bruno <engineering@usebruno.com>
362 lines
18 KiB
TypeScript
362 lines
18 KiB
TypeScript
import path from 'path';
|
|
import fs from 'fs';
|
|
import { test, expect, closeElectronApp } from '../../../playwright';
|
|
import {
|
|
createCollection,
|
|
createEnvironment,
|
|
openCollection,
|
|
openCollectionSettings,
|
|
selectEnvironment,
|
|
waitForReadyPage,
|
|
readSnapshot,
|
|
findSnapshotCollectionEntry,
|
|
waitForSnapshotCollectionEnvironment
|
|
} from '../../utils/page';
|
|
|
|
const legacyPromptVariablesInitUserDataPath = path.join(
|
|
__dirname,
|
|
'init-user-data'
|
|
);
|
|
|
|
const migrationCollectionPath = path.join(
|
|
__dirname,
|
|
'fixtures/collection'
|
|
);
|
|
|
|
test.describe('Snapshot: Collection Environment Persistence', () => {
|
|
test('migrates legacy snapshot format and preserves selected collection environment', async ({ launchElectronApp, createTmpDir }) => {
|
|
const userDataPath = await createTmpDir('snap-legacy-env-migration');
|
|
|
|
const app = await launchElectronApp({
|
|
initUserDataPath: legacyPromptVariablesInitUserDataPath,
|
|
userDataPath
|
|
});
|
|
const page = await waitForReadyPage(app);
|
|
|
|
await test.step('Verify legacy selected environment is hydrated in UI', async () => {
|
|
await openCollection(page, 'migration-collection');
|
|
await expect(page.locator('.current-environment')).toContainText('local');
|
|
});
|
|
|
|
await test.step('Close app and verify snapshot migrated to new shape', async () => {
|
|
await page.waitForTimeout(2000);
|
|
await closeElectronApp(app);
|
|
|
|
const snapshot = readSnapshot(userDataPath);
|
|
expect(snapshot).not.toBeNull();
|
|
expect(snapshot).toHaveProperty('version');
|
|
expect(snapshot).toHaveProperty('activeWorkspacePath');
|
|
expect(snapshot).toHaveProperty('extras');
|
|
expect(snapshot).toHaveProperty('workspaces');
|
|
expect(snapshot).toHaveProperty('collections');
|
|
expect(Array.isArray(snapshot?.workspaces)).toBe(true);
|
|
expect(Array.isArray(snapshot?.collections)).toBe(true);
|
|
|
|
const migratedCollectionEntry = findSnapshotCollectionEntry(snapshot, migrationCollectionPath);
|
|
expect(migratedCollectionEntry).toBeTruthy();
|
|
expect(migratedCollectionEntry?.selectedEnvironment).toBe('local');
|
|
});
|
|
});
|
|
|
|
test('keeps selected environments for non-active collections across snapshot saves', async ({ launchElectronApp, createTmpDir }) => {
|
|
const userDataPath = await createTmpDir('snap-env-persistence');
|
|
const firstCollectionPath = await createTmpDir('snap-col-a');
|
|
const secondCollectionPath = await createTmpDir('snap-col-b');
|
|
const firstCollectionRoot = path.join(firstCollectionPath, 'Collection A');
|
|
const secondCollectionRoot = path.join(secondCollectionPath, 'Collection B');
|
|
|
|
const app = await launchElectronApp({ userDataPath });
|
|
const page = await waitForReadyPage(app);
|
|
|
|
await test.step('Create two collections with distinct selected environments', async () => {
|
|
await createCollection(page, 'Collection A', firstCollectionPath);
|
|
await openCollection(page, 'Collection A');
|
|
await createEnvironment(page, 'local-a', 'collection');
|
|
await selectEnvironment(page, 'local-a', 'collection');
|
|
|
|
await createCollection(page, 'Collection B', secondCollectionPath);
|
|
await openCollection(page, 'Collection B');
|
|
await createEnvironment(page, 'local-b', 'collection');
|
|
await selectEnvironment(page, 'local-b', 'collection');
|
|
});
|
|
|
|
await test.step('Switch back to first collection and verify environment did not drift', async () => {
|
|
await openCollection(page, 'Collection A');
|
|
await expect(page.locator('.current-environment')).toContainText('local-a');
|
|
await openCollection(page, 'Collection B');
|
|
await expect(page.locator('.current-environment')).toContainText('local-b');
|
|
});
|
|
|
|
await test.step('Trigger snapshot save from active collection and verify both environments remain on disk', async () => {
|
|
await openCollectionSettings(page, 'Collection B', { persist: true });
|
|
|
|
await waitForSnapshotCollectionEnvironment(userDataPath, secondCollectionRoot, 'local-b');
|
|
await expect.poll(() => {
|
|
const snapshot = readSnapshot(userDataPath);
|
|
const firstEntry = findSnapshotCollectionEntry(snapshot, firstCollectionRoot);
|
|
const secondEntry = findSnapshotCollectionEntry(snapshot, secondCollectionRoot);
|
|
return Boolean(
|
|
firstEntry?.selectedEnvironment === 'local-a'
|
|
&& secondEntry?.selectedEnvironment === 'local-b'
|
|
);
|
|
}, { timeout: 15000 }).toBe(true);
|
|
});
|
|
|
|
await test.step('Close app and assert snapshot stores both environments', async () => {
|
|
await page.waitForTimeout(2000);
|
|
await closeElectronApp(app);
|
|
|
|
const snapshot = readSnapshot(userDataPath);
|
|
expect(snapshot).not.toBeNull();
|
|
const firstEntry = findSnapshotCollectionEntry(snapshot, firstCollectionRoot);
|
|
const secondEntry = findSnapshotCollectionEntry(snapshot, secondCollectionRoot);
|
|
|
|
expect(firstEntry?.selectedEnvironment).toBe('local-a');
|
|
expect(secondEntry?.selectedEnvironment).toBe('local-b');
|
|
expect(firstEntry?.environmentPath).toContain(path.join('environments', 'local-a'));
|
|
expect(secondEntry?.environmentPath).toContain(path.join('environments', 'local-b'));
|
|
});
|
|
|
|
await test.step('Restart app and verify both selections are still restored', async () => {
|
|
const app2 = await launchElectronApp({ userDataPath });
|
|
const page2 = await waitForReadyPage(app2);
|
|
|
|
await openCollection(page2, 'Collection A');
|
|
await expect(page2.locator('.current-environment')).toContainText('local-a');
|
|
|
|
// Wait for debounced snapshot save to flush to verify the snapshot isn't overwritten by the next save
|
|
await waitForSnapshotCollectionEnvironment(userDataPath, firstCollectionRoot, 'local-a');
|
|
|
|
await openCollection(page2, 'Collection B');
|
|
await expect(page2.locator('.current-environment')).toContainText('local-b');
|
|
|
|
// Wait for debounced snapshot save to flush to verify the snapshot isn't overwritten by the next save
|
|
await waitForSnapshotCollectionEnvironment(userDataPath, secondCollectionRoot, 'local-b');
|
|
|
|
await closeElectronApp(app2);
|
|
});
|
|
});
|
|
|
|
test('preserves unmounted collection environment after snapshot save from another collection', async ({ launchElectronApp, createTmpDir }) => {
|
|
test.setTimeout(60000);
|
|
const userDataPath = await createTmpDir('snap-env-unmounted-preserve');
|
|
const firstCollectionPath = await createTmpDir('snap-col-a-unmounted');
|
|
const secondCollectionPath = await createTmpDir('snap-col-b-unmounted');
|
|
const firstCollectionRoot = path.join(firstCollectionPath, 'Collection A');
|
|
const secondCollectionRoot = path.join(secondCollectionPath, 'Collection B');
|
|
|
|
const app = await launchElectronApp({ userDataPath });
|
|
const page = await waitForReadyPage(app);
|
|
|
|
await test.step('Create first collection environment and wait for snapshot persistence', async () => {
|
|
await createCollection(page, 'Collection A', firstCollectionPath);
|
|
await createEnvironment(page, 'local-a', 'collection');
|
|
await selectEnvironment(page, 'local-a', 'collection');
|
|
await waitForSnapshotCollectionEnvironment(userDataPath, firstCollectionRoot, 'local-a');
|
|
});
|
|
|
|
await test.step('Create second collection and make it active', async () => {
|
|
await createCollection(page, 'Collection B', secondCollectionPath);
|
|
await createEnvironment(page, 'local-b', 'collection');
|
|
await selectEnvironment(page, 'local-b', 'collection');
|
|
await waitForSnapshotCollectionEnvironment(userDataPath, secondCollectionRoot, 'local-b');
|
|
});
|
|
|
|
await test.step('Trigger debounced snapshot save while only second collection is active', async () => {
|
|
await openCollectionSettings(page, 'Collection B', { persist: true });
|
|
|
|
await expect.poll(() => {
|
|
const snapshot = readSnapshot(userDataPath);
|
|
const firstEntry = findSnapshotCollectionEntry(snapshot, firstCollectionRoot);
|
|
const secondEntry = findSnapshotCollectionEntry(snapshot, secondCollectionRoot);
|
|
return Boolean(
|
|
firstEntry?.selectedEnvironment === 'local-a'
|
|
&& firstEntry?.environmentPath?.includes('local-a')
|
|
&& secondEntry?.selectedEnvironment === 'local-b'
|
|
);
|
|
}, { timeout: 15000 }).toBe(true);
|
|
});
|
|
|
|
await closeElectronApp(app);
|
|
});
|
|
|
|
test('keeps selected environments for three collections across delayed switches and snapshot updates', async ({ launchElectronApp, createTmpDir }) => {
|
|
// Heavy test: 3 collections, several 2s settle waits, plus a full restart. The default
|
|
// budget collapses to 30s when the runner doesn't set CI=true, which isn't enough on
|
|
// slower machines (e.g. Windows) and times out mid-restart. Give it room.
|
|
test.setTimeout(60000);
|
|
const userDataPath = await createTmpDir('snap-env-persistence-three');
|
|
const firstCollectionPath = await createTmpDir('snap-col-a-three');
|
|
const secondCollectionPath = await createTmpDir('snap-col-b-three');
|
|
const thirdCollectionPath = await createTmpDir('snap-col-c-three');
|
|
const firstCollectionRoot = path.join(firstCollectionPath, 'Collection A');
|
|
const secondCollectionRoot = path.join(secondCollectionPath, 'Collection B');
|
|
const thirdCollectionRoot = path.join(thirdCollectionPath, 'Collection C');
|
|
|
|
const app = await launchElectronApp({ userDataPath });
|
|
const page = await waitForReadyPage(app);
|
|
|
|
await test.step('Create three collections with distinct selected environments', async () => {
|
|
await createCollection(page, 'Collection A', firstCollectionPath);
|
|
await openCollection(page, 'Collection A');
|
|
await createEnvironment(page, 'local-a', 'collection');
|
|
await selectEnvironment(page, 'local-a', 'collection');
|
|
|
|
await createCollection(page, 'Collection B', secondCollectionPath);
|
|
await openCollection(page, 'Collection B');
|
|
await createEnvironment(page, 'local-b', 'collection');
|
|
await selectEnvironment(page, 'local-b', 'collection');
|
|
|
|
await createCollection(page, 'Collection C', thirdCollectionPath);
|
|
await openCollection(page, 'Collection C');
|
|
await createEnvironment(page, 'local-c', 'collection');
|
|
await selectEnvironment(page, 'local-c', 'collection');
|
|
});
|
|
|
|
await test.step('Switch to each collection with delays and verify selected environment stays correct', async () => {
|
|
await openCollection(page, 'Collection A');
|
|
await expect(page.locator('.current-environment')).toContainText('local-a');
|
|
|
|
await openCollection(page, 'Collection B');
|
|
await expect(page.locator('.current-environment')).toContainText('local-b');
|
|
|
|
await openCollection(page, 'Collection C');
|
|
await expect(page.locator('.current-environment')).toContainText('local-c');
|
|
});
|
|
|
|
await test.step('Trigger snapshot save from active collection and verify all environments remain on disk', async () => {
|
|
await openCollectionSettings(page, 'Collection C', { persist: true });
|
|
|
|
await expect.poll(() => {
|
|
const snapshot = readSnapshot(userDataPath);
|
|
const firstEntry = findSnapshotCollectionEntry(snapshot, firstCollectionRoot);
|
|
const secondEntry = findSnapshotCollectionEntry(snapshot, secondCollectionRoot);
|
|
const thirdEntry = findSnapshotCollectionEntry(snapshot, thirdCollectionRoot);
|
|
return Boolean(
|
|
firstEntry?.selectedEnvironment === 'local-a'
|
|
&& secondEntry?.selectedEnvironment === 'local-b'
|
|
&& thirdEntry?.selectedEnvironment === 'local-c'
|
|
);
|
|
}, { timeout: 15000 }).toBe(true);
|
|
});
|
|
|
|
await test.step('Close app and assert snapshot stores all three environments', async () => {
|
|
await page.waitForTimeout(2000);
|
|
await closeElectronApp(app);
|
|
|
|
const snapshot = readSnapshot(userDataPath);
|
|
expect(snapshot).not.toBeNull();
|
|
const firstEntry = findSnapshotCollectionEntry(snapshot, firstCollectionRoot);
|
|
const secondEntry = findSnapshotCollectionEntry(snapshot, secondCollectionRoot);
|
|
const thirdEntry = findSnapshotCollectionEntry(snapshot, thirdCollectionRoot);
|
|
|
|
expect(firstEntry?.selectedEnvironment).toBe('local-a');
|
|
expect(secondEntry?.selectedEnvironment).toBe('local-b');
|
|
expect(thirdEntry?.selectedEnvironment).toBe('local-c');
|
|
expect(firstEntry?.environmentPath).toContain(path.join('environments', 'local-a'));
|
|
expect(secondEntry?.environmentPath).toContain(path.join('environments', 'local-b'));
|
|
expect(thirdEntry?.environmentPath).toContain(path.join('environments', 'local-c'));
|
|
});
|
|
|
|
await test.step('Restart app, switch through collections with delays, and verify all selections are restored', async () => {
|
|
const app2 = await launchElectronApp({ userDataPath });
|
|
const page2 = await waitForReadyPage(app2);
|
|
|
|
await openCollection(page2, 'Collection A');
|
|
await expect(page2.locator('.current-environment')).toContainText('local-a', { timeout: 15000 });
|
|
await waitForSnapshotCollectionEnvironment(userDataPath, firstCollectionRoot, 'local-a');
|
|
|
|
await openCollection(page2, 'Collection B');
|
|
await expect(page2.locator('.current-environment')).toContainText('local-b', { timeout: 15000 });
|
|
await waitForSnapshotCollectionEnvironment(userDataPath, secondCollectionRoot, 'local-b');
|
|
|
|
await openCollection(page2, 'Collection C');
|
|
await expect(page2.locator('.current-environment')).toContainText('local-c', { timeout: 15000 });
|
|
await waitForSnapshotCollectionEnvironment(userDataPath, thirdCollectionRoot, 'local-c');
|
|
|
|
await closeElectronApp(app2);
|
|
|
|
const updatedSnapshot = readSnapshot(userDataPath);
|
|
expect(updatedSnapshot).not.toBeNull();
|
|
|
|
const firstUpdatedEntry = findSnapshotCollectionEntry(updatedSnapshot, firstCollectionRoot);
|
|
const secondUpdatedEntry = findSnapshotCollectionEntry(updatedSnapshot, secondCollectionRoot);
|
|
const thirdUpdatedEntry = findSnapshotCollectionEntry(updatedSnapshot, thirdCollectionRoot);
|
|
|
|
expect(firstUpdatedEntry?.selectedEnvironment).toBe('local-a');
|
|
expect(secondUpdatedEntry?.selectedEnvironment).toBe('local-b');
|
|
expect(thirdUpdatedEntry?.selectedEnvironment).toBe('local-c');
|
|
});
|
|
});
|
|
|
|
test('preserves snapshot environment when referenced environment file is missing after restart', async ({ launchElectronApp, createTmpDir }) => {
|
|
const userDataPath = await createTmpDir('snap-missing-env');
|
|
const collectionPath = await createTmpDir('snap-missing-env-col');
|
|
const collectionRoot = path.join(collectionPath, 'Collection Missing Env');
|
|
|
|
const app = await launchElectronApp({ userDataPath });
|
|
const page = await waitForReadyPage(app);
|
|
|
|
await createCollection(page, 'Collection Missing Env', collectionPath);
|
|
await openCollection(page, 'Collection Missing Env');
|
|
await createEnvironment(page, 'local-missing', 'collection');
|
|
await selectEnvironment(page, 'local-missing', 'collection');
|
|
await page.waitForTimeout(1500);
|
|
await closeElectronApp(app);
|
|
|
|
const initialSnapshot = readSnapshot(userDataPath);
|
|
const initialEntry = findSnapshotCollectionEntry(initialSnapshot, collectionRoot);
|
|
const envFilePath = initialEntry?.environmentPath || initialEntry?.environment?.collection;
|
|
expect(envFilePath).toBeTruthy();
|
|
expect(fs.existsSync(envFilePath)).toBe(true);
|
|
fs.rmSync(envFilePath);
|
|
|
|
const app2 = await launchElectronApp({ userDataPath });
|
|
const page2 = await waitForReadyPage(app2);
|
|
await openCollection(page2, 'Collection Missing Env');
|
|
await expect(page2.locator('.current-environment')).toContainText('No Environment');
|
|
await waitForSnapshotCollectionEnvironment(userDataPath, collectionRoot, 'local-missing');
|
|
await closeElectronApp(app2);
|
|
|
|
const snapshot = readSnapshot(userDataPath);
|
|
const entry = findSnapshotCollectionEntry(snapshot, collectionRoot);
|
|
expect(entry?.selectedEnvironment).toBe('local-missing');
|
|
expect(entry?.environmentPath).toContain('local-missing');
|
|
});
|
|
|
|
test('preserves snapshot environment when environment file fails to parse after restart', async ({ launchElectronApp, createTmpDir }) => {
|
|
const userDataPath = await createTmpDir('snap-bad-env');
|
|
const collectionPath = await createTmpDir('snap-bad-env-col');
|
|
const collectionRoot = path.join(collectionPath, 'Collection Bad Env');
|
|
|
|
const app = await launchElectronApp({ userDataPath });
|
|
const page = await waitForReadyPage(app);
|
|
|
|
await createCollection(page, 'Collection Bad Env', collectionPath);
|
|
await openCollection(page, 'Collection Bad Env');
|
|
await createEnvironment(page, 'local-bad', 'collection');
|
|
await selectEnvironment(page, 'local-bad', 'collection');
|
|
await page.waitForTimeout(1500);
|
|
await closeElectronApp(app);
|
|
|
|
const initialSnapshot = readSnapshot(userDataPath);
|
|
const initialEntry = findSnapshotCollectionEntry(initialSnapshot, collectionRoot);
|
|
const envFilePath = initialEntry?.environmentPath || initialEntry?.environment?.collection;
|
|
expect(envFilePath).toBeTruthy();
|
|
expect(fs.existsSync(envFilePath)).toBe(true);
|
|
fs.writeFileSync(envFilePath, 'this is not valid environment syntax {{{');
|
|
|
|
const app2 = await launchElectronApp({ userDataPath });
|
|
const page2 = await waitForReadyPage(app2);
|
|
await openCollection(page2, 'Collection Bad Env');
|
|
await expect(page2.locator('.current-environment')).toContainText('local-bad');
|
|
await waitForSnapshotCollectionEnvironment(userDataPath, collectionRoot, 'local-bad');
|
|
await closeElectronApp(app2);
|
|
|
|
const snapshot = readSnapshot(userDataPath);
|
|
const entry = findSnapshotCollectionEntry(snapshot, collectionRoot);
|
|
expect(entry?.selectedEnvironment).toBe('local-bad');
|
|
expect(entry?.environmentPath).toContain('local-bad');
|
|
});
|
|
});
|