mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +00:00
- Added safeguards to prevent overwriting non-empty arrays with empty arrays during global settings updates, specifically for the 'projects' field. - Implemented logging for updates to assist in diagnosing accidental wipes of critical settings. - Updated tests to verify that projects are preserved during logout transitions and that theme changes are ignored if a project wipe is attempted. - Enhanced the settings synchronization logic to ensure safe handling during authentication state changes.
140 lines
5.2 KiB
TypeScript
140 lines
5.2 KiB
TypeScript
/**
|
||
* Settings Startup Race Regression Test
|
||
*
|
||
* Repro (historical bug):
|
||
* - UI verifies session successfully
|
||
* - Initial GET /api/settings/global fails transiently (backend still starting)
|
||
* - UI unblocks settings sync anyway and can push default empty state to server
|
||
* - Server persists projects: [] (and other defaults), wiping settings.json
|
||
*
|
||
* This test forces the first few /api/settings/global requests to fail and asserts that
|
||
* the server-side settings.json is NOT overwritten while the UI is waiting to hydrate.
|
||
*/
|
||
|
||
import { test, expect } from '@playwright/test';
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
import { authenticateForTests } from '../utils';
|
||
|
||
const SETTINGS_PATH = path.resolve(process.cwd(), '../server/data/settings.json');
|
||
const WORKSPACE_ROOT = path.resolve(process.cwd(), '../..');
|
||
const FIXTURE_PROJECT_PATH = path.join(WORKSPACE_ROOT, 'test/fixtures/projectA');
|
||
|
||
test.describe('Settings startup sync race', () => {
|
||
let originalSettingsJson: string;
|
||
|
||
test.beforeAll(() => {
|
||
originalSettingsJson = fs.readFileSync(SETTINGS_PATH, 'utf-8');
|
||
|
||
const settings = JSON.parse(originalSettingsJson) as Record<string, unknown>;
|
||
settings.projects = [
|
||
{
|
||
id: `e2e-project-${Date.now()}`,
|
||
name: 'E2E Project (settings race)',
|
||
path: FIXTURE_PROJECT_PATH,
|
||
lastOpened: new Date().toISOString(),
|
||
theme: 'dark',
|
||
},
|
||
];
|
||
|
||
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
||
});
|
||
|
||
test.afterAll(() => {
|
||
// Restore original settings.json to avoid polluting other tests/dev state
|
||
fs.writeFileSync(SETTINGS_PATH, originalSettingsJson);
|
||
});
|
||
|
||
test('does not overwrite projects when /api/settings/global is temporarily unavailable', async ({
|
||
page,
|
||
}) => {
|
||
// Gate the real settings request so we can assert file contents before allowing hydration.
|
||
let requestCount = 0;
|
||
let allowSettingsRequestResolve: (() => void) | null = null;
|
||
const allowSettingsRequest = new Promise<void>((resolve) => {
|
||
allowSettingsRequestResolve = resolve;
|
||
});
|
||
|
||
let sawThreeFailuresResolve: (() => void) | null = null;
|
||
const sawThreeFailures = new Promise<void>((resolve) => {
|
||
sawThreeFailuresResolve = resolve;
|
||
});
|
||
|
||
await page.route('**/api/settings/global', async (route) => {
|
||
requestCount++;
|
||
if (requestCount <= 3) {
|
||
if (requestCount === 3) {
|
||
sawThreeFailuresResolve?.();
|
||
}
|
||
await route.abort('failed');
|
||
return;
|
||
}
|
||
// Keep the 4th+ request pending until the test explicitly allows it.
|
||
await allowSettingsRequest;
|
||
await route.continue();
|
||
});
|
||
|
||
// Ensure we are authenticated (session cookie) before loading the app.
|
||
await authenticateForTests(page);
|
||
await page.goto('/');
|
||
|
||
// Wait until we have forced a few failures.
|
||
await sawThreeFailures;
|
||
|
||
// At this point, the UI should NOT have written defaults back to the server.
|
||
const settingsAfterFailures = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf-8')) as {
|
||
projects?: Array<{ path?: string }>;
|
||
};
|
||
expect(settingsAfterFailures.projects?.length).toBeGreaterThan(0);
|
||
expect(settingsAfterFailures.projects?.[0]?.path).toBe(FIXTURE_PROJECT_PATH);
|
||
|
||
// Allow the settings request to succeed so the app can hydrate and proceed.
|
||
allowSettingsRequestResolve?.();
|
||
|
||
// App should eventually render a main view after settings hydration.
|
||
await page
|
||
.locator('[data-testid="welcome-view"], [data-testid="board-view"]')
|
||
.first()
|
||
.waitFor({ state: 'visible', timeout: 30000 });
|
||
|
||
// Verify settings.json still contains the project after hydration completes.
|
||
const settingsAfterHydration = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf-8')) as {
|
||
projects?: Array<{ path?: string }>;
|
||
};
|
||
expect(settingsAfterHydration.projects?.length).toBeGreaterThan(0);
|
||
expect(settingsAfterHydration.projects?.[0]?.path).toBe(FIXTURE_PROJECT_PATH);
|
||
});
|
||
|
||
test('does not wipe projects during logout transition', async ({ page }) => {
|
||
// Ensure authenticated and app is loaded at least to welcome/board.
|
||
await authenticateForTests(page);
|
||
await page.goto('/');
|
||
await page
|
||
.locator('[data-testid="welcome-view"], [data-testid="board-view"]')
|
||
.first()
|
||
.waitFor({ state: 'visible', timeout: 30000 });
|
||
|
||
// Confirm settings.json currently has projects (precondition).
|
||
const beforeLogout = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf-8')) as {
|
||
projects?: Array<unknown>;
|
||
};
|
||
expect(beforeLogout.projects?.length).toBeGreaterThan(0);
|
||
|
||
// Navigate to settings and click logout.
|
||
await page.goto('/settings');
|
||
await page.locator('[data-testid="logout-button"]').click();
|
||
|
||
// Ensure we landed on logged-out or login (either is acceptable).
|
||
await page
|
||
.locator('text=You’ve been logged out, text=Authentication Required')
|
||
.first()
|
||
.waitFor({ state: 'visible', timeout: 30000 });
|
||
|
||
// The server settings file should still have projects after logout.
|
||
const afterLogout = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf-8')) as {
|
||
projects?: Array<unknown>;
|
||
};
|
||
expect(afterLogout.projects?.length).toBeGreaterThan(0);
|
||
});
|
||
});
|