- prettier formatting across routes, components and tests (no behavior change) - unused imports and ternary-as-statement replaced with explicit branches - eslint: scripts/**/*.js are CommonJS, allow require() there; prettier ignores pnpm-lock.yaml
180 lines
6.0 KiB
TypeScript
180 lines
6.0 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import {
|
|
listAccountsData,
|
|
addAccountSnapshot,
|
|
switchAccount,
|
|
deleteAccount,
|
|
} from '../../ipc/account/handler';
|
|
import { restoreAccount } from '../../ipc/database/handler';
|
|
import { startGeminiNexus } 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 () => {
|
|
const path = await import('path');
|
|
const agentDir = path.join(process.cwd(), 'temp_test_agent');
|
|
return {
|
|
IS_DEV_ENVIRONMENT: false,
|
|
getAgentDir: vi.fn(() => agentDir),
|
|
getAccountsFilePath: vi.fn(() => path.join(agentDir, 'accounts.json')),
|
|
getBackupsDir: vi.fn(() => path.join(agentDir, 'backups')),
|
|
getGeminiNexusDbPath: vi.fn(() => path.join(agentDir, 'state.vscdb')),
|
|
getGeminiNexusExecutablePath: vi.fn(() => 'mock_exec_path'),
|
|
};
|
|
});
|
|
|
|
vi.mock('../../ipc/database/handler', () => ({
|
|
getCurrentAccountInfo: vi.fn(() => ({
|
|
email: '[email protected]',
|
|
name: 'Test User',
|
|
isAuthenticated: true,
|
|
})),
|
|
backupAccount: vi.fn((account) => ({ version: '1.0', account, data: {} })),
|
|
restoreAccount: vi.fn(),
|
|
getDatabaseConnection: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../../ipc/process/handler', () => ({
|
|
closeGeminiNexus: vi.fn(),
|
|
startGeminiNexus: 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 });
|
|
}
|
|
fs.mkdirSync(testAgentDir, { recursive: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (fs.existsSync(testAgentDir)) {
|
|
fs.rmSync(testAgentDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('should add account snapshot', async () => {
|
|
const account = await addAccountSnapshot();
|
|
expect(account.email).toBe('[email protected]');
|
|
|
|
const accounts = await listAccountsData();
|
|
expect(accounts).toHaveLength(1);
|
|
expect(accounts[0].email).toBe('[email protected]');
|
|
});
|
|
|
|
it('should switch account', async () => {
|
|
const account = await addAccountSnapshot();
|
|
await switchAccount(account.id);
|
|
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 () => {
|
|
const account = await addAccountSnapshot();
|
|
await deleteAccount(account.id);
|
|
|
|
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(startGeminiNexus).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should queue switch requests instead of rejecting concurrent calls', async () => {
|
|
const account = await addAccountSnapshot();
|
|
|
|
const startMock = vi.mocked(startGeminiNexus);
|
|
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);
|
|
});
|
|
});
|