Files
bruno/tests/snapshots/environment/environment.spec.ts
Sid b1f08b5131 fix(snapshot): active workspace restoration from snapshot (#8392)
* 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>
2026-07-04 17:38:41 +05:30

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');
});
});