feat: complete account-bound profile switching and hardening (#78)
This commit is contained in:
31 files changed
+3612
-197
No files matched your search
@@ -5,8 +5,12 @@ import {
|
||||
switchAccount,
|
||||
deleteAccount,
|
||||
} from '../../ipc/account/handler';
|
||||
import { restoreAccount } from '../../ipc/database/handler';
|
||||
import { startAntigravity } from '../../ipc/process/handler';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { applyDeviceProfile, generateDeviceProfile } from '../../ipc/device/handler';
|
||||
import { getSwitchGuardSnapshot } from '../../ipc/switchGuard';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../../utils/paths', async () => {
|
||||
@@ -35,13 +39,35 @@ vi.mock('../../ipc/database/handler', () => ({
|
||||
vi.mock('../../ipc/process/handler', () => ({
|
||||
closeAntigravity: vi.fn(),
|
||||
startAntigravity: vi.fn(),
|
||||
_waitForProcessExit: vi.fn(),
|
||||
isProcessRunning: vi.fn(() => Promise.resolve(false)),
|
||||
}));
|
||||
|
||||
vi.mock('../../ipc/device/handler', () => ({
|
||||
applyDeviceProfile: vi.fn(),
|
||||
ensureGlobalOriginalFromCurrentStorage: vi.fn(),
|
||||
generateDeviceProfile: vi.fn(() => ({
|
||||
machineId: 'auth0|user_test',
|
||||
macMachineId: 'mac-machine-id',
|
||||
devDeviceId: 'dev-device-id',
|
||||
sqmId: '{SQM-ID}',
|
||||
})),
|
||||
loadGlobalOriginalProfile: vi.fn(() => null),
|
||||
isIdentityProfileApplyEnabled: vi.fn(() => true),
|
||||
readCurrentDeviceProfile: vi.fn(() => ({
|
||||
machineId: 'current-machine-id',
|
||||
macMachineId: 'current-mac-machine-id',
|
||||
devDeviceId: 'current-dev-device-id',
|
||||
sqmId: '{CURRENT-SQM-ID}',
|
||||
})),
|
||||
saveGlobalOriginalProfile: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('Account Handler', () => {
|
||||
const testAgentDir = path.join(process.cwd(), 'temp_test_agent');
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
if (fs.existsSync(testAgentDir)) {
|
||||
fs.rmSync(testAgentDir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -66,7 +92,29 @@ describe('Account Handler', () => {
|
||||
it('should switch account', async () => {
|
||||
const account = await addAccountSnapshot();
|
||||
await switchAccount(account.id);
|
||||
// Mocks should be called, verified by implementation logic not throwing
|
||||
expect(generateDeviceProfile).toHaveBeenCalled();
|
||||
expect(applyDeviceProfile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reuse existing device profile on switch', async () => {
|
||||
const account = await addAccountSnapshot();
|
||||
const accountFilePath = path.join(testAgentDir, 'accounts.json');
|
||||
const allAccounts = JSON.parse(fs.readFileSync(accountFilePath, 'utf-8')) as Record<string, any>;
|
||||
allAccounts[account.id].deviceProfile = {
|
||||
machineId: 'existing-machine',
|
||||
macMachineId: 'existing-mac',
|
||||
devDeviceId: 'existing-dev',
|
||||
sqmId: '{EXISTING-SQM}',
|
||||
};
|
||||
fs.writeFileSync(accountFilePath, JSON.stringify(allAccounts, null, 2), 'utf-8');
|
||||
|
||||
await switchAccount(account.id);
|
||||
expect(applyDeviceProfile).toHaveBeenCalledWith({
|
||||
machineId: 'existing-machine',
|
||||
macMachineId: 'existing-mac',
|
||||
devDeviceId: 'existing-dev',
|
||||
sqmId: '{EXISTING-SQM}',
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete account', async () => {
|
||||
@@ -76,4 +124,52 @@ describe('Account Handler', () => {
|
||||
const accounts = await listAccountsData();
|
||||
expect(accounts).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should fail fast without rollback or forced restart when restore fails', async () => {
|
||||
const restoreMock = vi.mocked(restoreAccount);
|
||||
restoreMock.mockImplementationOnce(() => {
|
||||
throw new Error('restore_failed');
|
||||
});
|
||||
|
||||
const account = await addAccountSnapshot();
|
||||
await expect(switchAccount(account.id)).rejects.toThrow('restore_failed');
|
||||
|
||||
expect(applyDeviceProfile).toHaveBeenCalledTimes(1);
|
||||
expect(applyDeviceProfile).toHaveBeenCalledWith({
|
||||
machineId: 'auth0|user_test',
|
||||
macMachineId: 'mac-machine-id',
|
||||
devDeviceId: 'dev-device-id',
|
||||
sqmId: '{SQM-ID}',
|
||||
});
|
||||
expect(startAntigravity).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should queue switch requests instead of rejecting concurrent calls', async () => {
|
||||
const account = await addAccountSnapshot();
|
||||
|
||||
const startMock = vi.mocked(startAntigravity);
|
||||
let releaseFirstStart!: () => void;
|
||||
const firstStartBlocker = new Promise<void>((resolve) => {
|
||||
releaseFirstStart = resolve;
|
||||
});
|
||||
startMock.mockImplementationOnce(async () => {
|
||||
await firstStartBlocker;
|
||||
});
|
||||
|
||||
const firstSwitch = switchAccount(account.id);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
const secondSwitch = switchAccount(account.id);
|
||||
|
||||
const runningSnapshot = getSwitchGuardSnapshot();
|
||||
expect(runningSnapshot.activeOwner).toBe('local-account-switch');
|
||||
expect(runningSnapshot.pendingCount).toBeGreaterThanOrEqual(1);
|
||||
|
||||
releaseFirstStart();
|
||||
|
||||
await Promise.all([firstSwitch, secondSwitch]);
|
||||
|
||||
const finalSnapshot = getSwitchGuardSnapshot();
|
||||
expect(finalSnapshot.activeOwner).toBeNull();
|
||||
expect(finalSnapshot.pendingCount).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -266,4 +266,124 @@ describe('CloudAccountRepo.syncFromIDE', () => {
|
||||
expect(wroteUnifiedKey).toBe(true);
|
||||
expect(updatedOldKey).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('cloud switch fail-fast path', () => {
|
||||
afterEach(() => {
|
||||
vi.resetModules();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should fail fast without rollback or forced restart when inject fails', async () => {
|
||||
vi.resetModules();
|
||||
|
||||
const applyDeviceProfileMock = vi.fn();
|
||||
const startAntigravityMock = vi.fn(async () => undefined);
|
||||
const recordSwitchFailureMock = vi.fn();
|
||||
const recordSwitchSuccessMock = vi.fn();
|
||||
|
||||
const account = {
|
||||
id: 'acc-1',
|
||||
email: '[email protected]',
|
||||
name: 'Cloud User',
|
||||
provider: 'google' as const,
|
||||
token: {
|
||||
access_token: 'access',
|
||||
refresh_token: 'refresh',
|
||||
expires_in: 3600,
|
||||
expiry_timestamp: Math.floor(Date.now() / 1000) + 3600,
|
||||
token_type: 'Bearer',
|
||||
email: '[email protected]',
|
||||
},
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
last_used: Math.floor(Date.now() / 1000),
|
||||
device_profile: {
|
||||
machineId: 'target-machine',
|
||||
macMachineId: 'target-mac',
|
||||
devDeviceId: 'target-dev',
|
||||
sqmId: '{TARGET-SQM}',
|
||||
},
|
||||
};
|
||||
|
||||
vi.doMock('../../ipc/database/cloudHandler', () => ({
|
||||
CloudAccountRepo: {
|
||||
getAccount: vi.fn(async () => account),
|
||||
setDeviceBinding: vi.fn(),
|
||||
updateToken: vi.fn(async () => undefined),
|
||||
injectCloudToken: vi.fn(() => {
|
||||
throw new Error('inject_failed');
|
||||
}),
|
||||
updateLastUsed: vi.fn(),
|
||||
setActive: vi.fn(),
|
||||
getSetting: vi.fn(() => 'en'),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.doMock('../../ipc/device/handler', () => ({
|
||||
applyDeviceProfile: applyDeviceProfileMock,
|
||||
ensureGlobalOriginalFromCurrentStorage: vi.fn(),
|
||||
generateDeviceProfile: vi.fn(() => account.device_profile),
|
||||
isIdentityProfileApplyEnabled: vi.fn(() => true),
|
||||
readCurrentDeviceProfile: vi.fn(() => ({
|
||||
machineId: 'prev-machine',
|
||||
macMachineId: 'prev-mac',
|
||||
devDeviceId: 'prev-dev',
|
||||
sqmId: '{PREV-SQM}',
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.doMock('../../ipc/process/handler', () => ({
|
||||
closeAntigravity: vi.fn(async () => undefined),
|
||||
startAntigravity: startAntigravityMock,
|
||||
_waitForProcessExit: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
vi.doMock('../../ipc/switchMetrics', () => ({
|
||||
recordSwitchFailure: recordSwitchFailureMock,
|
||||
recordSwitchSuccess: recordSwitchSuccessMock,
|
||||
}));
|
||||
|
||||
vi.doMock('../../ipc/tray/handler', () => ({
|
||||
updateTrayMenu: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.doMock('../../utils/paths', () => ({
|
||||
getAntigravityDbPaths: () => [],
|
||||
}));
|
||||
|
||||
vi.doMock('../../utils/logger', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.doMock('../../services/GoogleAPIService', () => ({
|
||||
GoogleAPIService: {
|
||||
refreshAccessToken: vi.fn(async () => undefined),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.doMock('electron', () => ({
|
||||
shell: {
|
||||
openExternal: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const { switchCloudAccount } = await import('../../ipc/cloud/handler');
|
||||
await expect(switchCloudAccount('acc-1')).rejects.toThrow('Switch failed: inject_failed');
|
||||
|
||||
expect(applyDeviceProfileMock).toHaveBeenCalledTimes(1);
|
||||
expect(applyDeviceProfileMock).toHaveBeenCalledWith(account.device_profile);
|
||||
expect(startAntigravityMock).not.toHaveBeenCalled();
|
||||
expect(recordSwitchFailureMock).toHaveBeenCalledWith(
|
||||
'cloud',
|
||||
'perform_switch_failed',
|
||||
expect.stringContaining('inject_failed'),
|
||||
);
|
||||
expect(recordSwitchSuccessMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
getAppDataDir,
|
||||
getAntigravityDbPath,
|
||||
getAntigravityStoragePath,
|
||||
getAntigravityExecutablePath,
|
||||
} from '../../utils/paths';
|
||||
|
||||
@@ -17,6 +18,11 @@ describe('Path Utilities', () => {
|
||||
expect(dbPath).toContain('state.vscdb');
|
||||
});
|
||||
|
||||
it('should get correct storage path', () => {
|
||||
const storagePath = getAntigravityStoragePath();
|
||||
expect(storagePath).toContain('storage.json');
|
||||
});
|
||||
|
||||
it('should get correct executable path', () => {
|
||||
const execPath = getAntigravityExecutablePath();
|
||||
if (process.platform === 'linux') {
|
||||
|
||||
Reference in new issue
Block a user