279 lines
9.2 KiB
TypeScript
279 lines
9.2 KiB
TypeScript
/**
|
|
* 单元测试: migrate (数据目录迁移)
|
|
*
|
|
* 覆盖迁移路径:
|
|
* 1. .qiming-agent → .qimingclaw (优先级最高)
|
|
* 2. .qimingbot → .qimingclaw
|
|
* 3. 无旧目录 → 跳过
|
|
* 4. 新目录已存在但 DB 为空 → 从旧目录导入 DB
|
|
* 5. 新目录已存在且 DB 有数据 → 跳过
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import * as path from 'path';
|
|
|
|
vi.mock('electron', () => ({
|
|
app: { getPath: vi.fn(() => '/mock/home') },
|
|
}));
|
|
|
|
vi.mock('electron-log', () => ({
|
|
default: { info: vi.fn(), error: vi.fn(), warn: vi.fn() },
|
|
}));
|
|
|
|
const mockExistsSync = vi.fn(() => false);
|
|
const mockRenameSync = vi.fn();
|
|
const mockCopyFileSync = vi.fn();
|
|
|
|
vi.mock('fs', () => ({
|
|
existsSync: (p: string) => mockExistsSync(p),
|
|
renameSync: (o: string, n: string) => mockRenameSync(o, n),
|
|
copyFileSync: (o: string, n: string) => mockCopyFileSync(o, n),
|
|
}));
|
|
|
|
const mockReadSetting = vi.fn(() => null);
|
|
const mockWriteSetting = vi.fn();
|
|
|
|
vi.mock('../db', () => ({
|
|
readSetting: (...args: unknown[]) => mockReadSetting(...args),
|
|
writeSetting: (...args: unknown[]) => mockWriteSetting(...args),
|
|
}));
|
|
|
|
// Mock better-sqlite3 for isDbEmpty checks
|
|
const mockDbPrepare = vi.fn();
|
|
const mockDbClose = vi.fn();
|
|
|
|
vi.mock('better-sqlite3', () => ({
|
|
default: vi.fn(() => ({
|
|
prepare: mockDbPrepare,
|
|
close: mockDbClose,
|
|
})),
|
|
}));
|
|
|
|
describe('migrateDataDir', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
// Default: isDbEmpty returns true (count=0)
|
|
mockDbPrepare.mockReturnValue({ get: () => ({ count: 0 }) });
|
|
});
|
|
|
|
it('should skip when no legacy directory exists', async () => {
|
|
mockExistsSync.mockReturnValue(false);
|
|
|
|
const { migrateDataDir } = await import('./migrate');
|
|
migrateDataDir();
|
|
|
|
expect(mockRenameSync).not.toHaveBeenCalled();
|
|
expect(mockCopyFileSync).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should skip when new directory exists and DB has data', async () => {
|
|
// new dir exists, new DB exists and has data
|
|
mockExistsSync.mockImplementation((p: string) => {
|
|
if (p.includes('.qimingclaw')) return true;
|
|
return false;
|
|
});
|
|
// DB has data (count > 0)
|
|
mockDbPrepare.mockReturnValue({ get: () => ({ count: 5 }) });
|
|
|
|
const { migrateDataDir } = await import('./migrate');
|
|
migrateDataDir();
|
|
|
|
expect(mockRenameSync).not.toHaveBeenCalled();
|
|
expect(mockCopyFileSync).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should import legacy DB when new dir exists but DB is empty', async () => {
|
|
const newDb = path.join('/mock/home', '.qimingclaw', 'qimingclaw.db');
|
|
const oldDb = path.join('/mock/home', '.qimingbot', 'qimingbot.db');
|
|
|
|
mockExistsSync.mockImplementation((p: string) => {
|
|
if (p === path.join('/mock/home', '.qimingclaw')) return true;
|
|
if (p === newDb) return true;
|
|
if (p === oldDb) return true;
|
|
if (p.endsWith('-wal') || p.endsWith('-shm')) return false;
|
|
return false;
|
|
});
|
|
|
|
// First call: new DB is empty (count=0), second call: old DB has data (count=5)
|
|
mockDbPrepare
|
|
.mockReturnValueOnce({ get: () => ({ count: 0 }) })
|
|
.mockReturnValueOnce({ get: () => ({ count: 5 }) });
|
|
|
|
const { migrateDataDir } = await import('./migrate');
|
|
migrateDataDir();
|
|
|
|
expect(mockCopyFileSync).toHaveBeenCalledWith(oldDb, newDb);
|
|
expect(mockRenameSync).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should migrate .qiming-agent → .qimingclaw (priority 1)', async () => {
|
|
mockExistsSync.mockImplementation((p: string) => {
|
|
if (p === path.join('/mock/home', '.qimingclaw')) return false;
|
|
if (p === path.join('/mock/home', '.qiming-agent')) return true;
|
|
if (p.endsWith('qiming-agent.db')) return true;
|
|
if (p.endsWith('qimingclaw.db')) return false;
|
|
if (p.endsWith('-wal') || p.endsWith('-shm')) return false;
|
|
return false;
|
|
});
|
|
|
|
const { migrateDataDir } = await import('./migrate');
|
|
migrateDataDir();
|
|
|
|
// Directory rename
|
|
expect(mockRenameSync).toHaveBeenCalledWith(
|
|
path.join('/mock/home', '.qiming-agent'),
|
|
path.join('/mock/home', '.qimingclaw'),
|
|
);
|
|
|
|
// DB rename
|
|
expect(mockRenameSync).toHaveBeenCalledWith(
|
|
path.join('/mock/home', '.qimingclaw', 'qiming-agent.db'),
|
|
path.join('/mock/home', '.qimingclaw', 'qimingclaw.db'),
|
|
);
|
|
});
|
|
|
|
it('should migrate .qimingbot → .qimingclaw (priority 2)', async () => {
|
|
mockExistsSync.mockImplementation((p: string) => {
|
|
if (p === path.join('/mock/home', '.qimingclaw')) return false;
|
|
if (p === path.join('/mock/home', '.qiming-agent')) return false;
|
|
if (p === path.join('/mock/home', '.qimingbot')) return true;
|
|
if (p.endsWith('qimingbot.db')) return true;
|
|
if (p.endsWith('qimingclaw.db')) return false;
|
|
if (p.endsWith('qimingbot.json')) return true;
|
|
if (p.endsWith('qimingclaw.json')) return false;
|
|
if (p.endsWith('-wal') || p.endsWith('-shm')) return false;
|
|
return false;
|
|
});
|
|
|
|
const { migrateDataDir } = await import('./migrate');
|
|
migrateDataDir();
|
|
|
|
// Directory rename
|
|
expect(mockRenameSync).toHaveBeenCalledWith(
|
|
path.join('/mock/home', '.qimingbot'),
|
|
path.join('/mock/home', '.qimingclaw'),
|
|
);
|
|
|
|
// DB rename
|
|
expect(mockRenameSync).toHaveBeenCalledWith(
|
|
path.join('/mock/home', '.qimingclaw', 'qimingbot.db'),
|
|
path.join('/mock/home', '.qimingclaw', 'qimingclaw.db'),
|
|
);
|
|
|
|
// Config rename
|
|
expect(mockRenameSync).toHaveBeenCalledWith(
|
|
path.join('/mock/home', '.qimingclaw', 'qimingbot.json'),
|
|
path.join('/mock/home', '.qimingclaw', 'qimingclaw.json'),
|
|
);
|
|
});
|
|
|
|
it('should also rename WAL and SHM files if they exist', async () => {
|
|
mockExistsSync.mockImplementation((p: string) => {
|
|
if (p === path.join('/mock/home', '.qimingclaw')) return false;
|
|
if (p === path.join('/mock/home', '.qiming-agent')) return true;
|
|
if (p.endsWith('qiming-agent.db')) return true;
|
|
if (p.endsWith('qimingclaw.db')) return false;
|
|
if (p.endsWith('qiming-agent.db-wal')) return true;
|
|
if (p.endsWith('qiming-agent.db-shm')) return true;
|
|
return false;
|
|
});
|
|
|
|
const { migrateDataDir } = await import('./migrate');
|
|
migrateDataDir();
|
|
|
|
expect(mockRenameSync).toHaveBeenCalledWith(
|
|
path.join('/mock/home', '.qimingclaw', 'qiming-agent.db-wal'),
|
|
path.join('/mock/home', '.qimingclaw', 'qimingclaw.db-wal'),
|
|
);
|
|
expect(mockRenameSync).toHaveBeenCalledWith(
|
|
path.join('/mock/home', '.qimingclaw', 'qiming-agent.db-shm'),
|
|
path.join('/mock/home', '.qimingclaw', 'qimingclaw.db-shm'),
|
|
);
|
|
});
|
|
|
|
it('should prefer .qiming-agent over .qimingbot when both exist', async () => {
|
|
mockExistsSync.mockImplementation((p: string) => {
|
|
if (p === path.join('/mock/home', '.qimingclaw')) return false;
|
|
if (p === path.join('/mock/home', '.qiming-agent')) return true;
|
|
if (p === path.join('/mock/home', '.qimingbot')) return true;
|
|
if (p.endsWith('qiming-agent.db')) return true;
|
|
if (p.endsWith('qimingclaw.db')) return false;
|
|
return false;
|
|
});
|
|
|
|
const { migrateDataDir } = await import('./migrate');
|
|
migrateDataDir();
|
|
|
|
// Should rename .qiming-agent, NOT .qimingbot
|
|
expect(mockRenameSync).toHaveBeenCalledWith(
|
|
path.join('/mock/home', '.qiming-agent'),
|
|
path.join('/mock/home', '.qimingclaw'),
|
|
);
|
|
expect(mockRenameSync).not.toHaveBeenCalledWith(
|
|
path.join('/mock/home', '.qimingbot'),
|
|
expect.anything(),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('migrateSettingsPaths', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('should skip when step1_config is null', async () => {
|
|
mockReadSetting.mockReturnValue(null);
|
|
|
|
const { migrateSettingsPaths } = await import('./migrate');
|
|
migrateSettingsPaths();
|
|
|
|
expect(mockWriteSetting).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should skip when workspaceDir is not a string', async () => {
|
|
mockReadSetting.mockReturnValue({ serverHost: 'example.com' });
|
|
|
|
const { migrateSettingsPaths } = await import('./migrate');
|
|
migrateSettingsPaths();
|
|
|
|
expect(mockWriteSetting).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should skip when workspaceDir does not match legacy prefix', async () => {
|
|
mockReadSetting.mockReturnValue({ workspaceDir: '/Users/user/projects' });
|
|
|
|
const { migrateSettingsPaths } = await import('./migrate');
|
|
migrateSettingsPaths();
|
|
|
|
expect(mockWriteSetting).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should update workspaceDir from .qimingbot prefix', async () => {
|
|
mockReadSetting.mockReturnValue({
|
|
workspaceDir: path.join('/mock/home', '.qimingbot', 'workspace'),
|
|
serverHost: 'example.com',
|
|
});
|
|
|
|
const { migrateSettingsPaths } = await import('./migrate');
|
|
migrateSettingsPaths();
|
|
|
|
expect(mockWriteSetting).toHaveBeenCalledWith('step1_config', {
|
|
workspaceDir: path.join('/mock/home', '.qimingclaw', 'workspace'),
|
|
serverHost: 'example.com',
|
|
});
|
|
});
|
|
|
|
it('should update workspaceDir from .qiming-agent prefix', async () => {
|
|
mockReadSetting.mockReturnValue({
|
|
workspaceDir: path.join('/mock/home', '.qiming-agent', 'workspace'),
|
|
});
|
|
|
|
const { migrateSettingsPaths } = await import('./migrate');
|
|
migrateSettingsPaths();
|
|
|
|
expect(mockWriteSetting).toHaveBeenCalledWith('step1_config', {
|
|
workspaceDir: path.join('/mock/home', '.qimingclaw', 'workspace'),
|
|
});
|
|
});
|
|
});
|