mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-05 05:59:43 +08:00
- 后端接入:新增独立 starrocks 可选驱动,复用 MySQL wire 协议并支持默认 9030 端口 - 驱动管理:补齐 manifest、build tag、revision、driver-agent provider 和构建脚本 - 前端接入:新增 StarRocks 连接类型、图标、能力矩阵、URI 解析、保存回显和 SQL 自动 LIMIT - 方言增强:新增 StarRocks 类型、关键字、函数补全和专属建表 SQL 生成 - 高级对象:支持物化视图对象浏览、Rollup 模板、外部 Catalog 模板和高级表设计器参数 - CI 发布:将 StarRocks driver-agent 纳入 dev/release 构建与 release 资产校验
608 lines
17 KiB
TypeScript
608 lines
17 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
class MemoryStorage implements Storage {
|
|
private data = new Map<string, string>();
|
|
|
|
get length(): number {
|
|
return this.data.size;
|
|
}
|
|
|
|
clear(): void {
|
|
this.data.clear();
|
|
}
|
|
|
|
getItem(key: string): string | null {
|
|
return this.data.has(key) ? this.data.get(key)! : null;
|
|
}
|
|
|
|
key(index: number): string | null {
|
|
return Array.from(this.data.keys())[index] ?? null;
|
|
}
|
|
|
|
removeItem(key: string): void {
|
|
this.data.delete(key);
|
|
}
|
|
|
|
setItem(key: string, value: string): void {
|
|
this.data.set(key, String(value));
|
|
}
|
|
}
|
|
|
|
const importStore = async () => {
|
|
const store = await import('./store');
|
|
await store.useStore.persist.rehydrate();
|
|
return store;
|
|
};
|
|
|
|
describe('store appearance persistence', () => {
|
|
let storage: MemoryStorage;
|
|
|
|
beforeEach(() => {
|
|
storage = new MemoryStorage();
|
|
vi.stubGlobal('localStorage', storage);
|
|
vi.resetModules();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
vi.resetModules();
|
|
});
|
|
|
|
it('fills missing DataGrid appearance settings with defaults during hydration', async () => {
|
|
storage.setItem('lite-db-storage', JSON.stringify({
|
|
state: {
|
|
appearance: {
|
|
enabled: false,
|
|
opacity: 0.75,
|
|
blur: 6,
|
|
useNativeMacWindowControls: true,
|
|
},
|
|
},
|
|
version: 7,
|
|
}));
|
|
|
|
const { useStore } = await importStore();
|
|
const appearance = useStore.getState().appearance;
|
|
|
|
expect(appearance.enabled).toBe(false);
|
|
expect(appearance.opacity).toBe(0.75);
|
|
expect(appearance.blur).toBe(6);
|
|
expect(appearance.useNativeMacWindowControls).toBe(true);
|
|
expect(appearance.showDataTableVerticalBorders).toBe(false);
|
|
expect(appearance.dataTableDensity).toBe('comfortable');
|
|
});
|
|
|
|
it('persists DataGrid appearance settings and restores them after reload', async () => {
|
|
const { useStore } = await importStore();
|
|
|
|
useStore.getState().setAppearance({
|
|
showDataTableVerticalBorders: true,
|
|
dataTableDensity: 'compact',
|
|
});
|
|
|
|
const persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}');
|
|
expect(persisted.state.appearance.showDataTableVerticalBorders).toBe(true);
|
|
expect(persisted.state.appearance.dataTableDensity).toBe('compact');
|
|
|
|
vi.resetModules();
|
|
const reloaded = await importStore();
|
|
const appearance = reloaded.useStore.getState().appearance;
|
|
|
|
expect(appearance.showDataTableVerticalBorders).toBe(true);
|
|
expect(appearance.dataTableDensity).toBe('compact');
|
|
});
|
|
|
|
it('does not clear persisted legacy connections during hydration migration', async () => {
|
|
storage.setItem('lite-db-storage', JSON.stringify({
|
|
state: {
|
|
connections: [
|
|
{
|
|
id: 'legacy-1',
|
|
name: 'Legacy',
|
|
config: {
|
|
id: 'legacy-1',
|
|
type: 'postgres',
|
|
host: 'db.local',
|
|
port: 5432,
|
|
user: 'postgres',
|
|
password: 'secret',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
version: 7,
|
|
}));
|
|
|
|
const { useStore } = await importStore();
|
|
|
|
expect(useStore.getState().connections).toHaveLength(1);
|
|
expect(useStore.getState().connections[0]?.config.password).toBe('secret');
|
|
});
|
|
|
|
it('does not fail hydration when persisted OceanBase connection uses unsupported native protocol', async () => {
|
|
storage.setItem('lite-db-storage', JSON.stringify({
|
|
state: {
|
|
connections: [
|
|
{
|
|
id: 'oceanbase-native',
|
|
name: 'OceanBase Native',
|
|
config: {
|
|
id: 'oceanbase-native',
|
|
type: 'oceanbase',
|
|
host: 'ob.local',
|
|
port: 2881,
|
|
user: 'root@test',
|
|
oceanBaseProtocol: 'mysql',
|
|
connectionParams: 'protocol=native',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
version: 9,
|
|
}));
|
|
|
|
const { useStore } = await importStore();
|
|
const config = useStore.getState().connections[0]?.config;
|
|
|
|
expect(useStore.getState().connections).toHaveLength(1);
|
|
expect(config?.connectionParams).toBe('protocol=native');
|
|
expect(config?.oceanBaseProtocol).toBe('mysql');
|
|
});
|
|
|
|
it('preserves JVM Arthas diagnostic config when replacing saved connections', async () => {
|
|
const { useStore } = await importStore();
|
|
|
|
useStore.getState().replaceConnections([
|
|
{
|
|
id: 'jvm-1',
|
|
name: 'Orders JVM',
|
|
config: {
|
|
id: 'jvm-1',
|
|
type: 'jvm',
|
|
host: '127.0.0.1',
|
|
port: 9010,
|
|
user: '',
|
|
jvm: {
|
|
allowedModes: ['jmx'],
|
|
preferredMode: 'jmx',
|
|
diagnostic: {
|
|
enabled: true,
|
|
transport: 'arthas-tunnel',
|
|
baseUrl: 'http://127.0.0.1:7777',
|
|
targetId: 'gonavi-local-test',
|
|
apiKey: 'diag-token',
|
|
allowObserveCommands: true,
|
|
allowTraceCommands: true,
|
|
allowMutatingCommands: false,
|
|
timeoutSeconds: 20,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
]);
|
|
|
|
expect(useStore.getState().connections[0]?.config.jvm?.diagnostic).toEqual({
|
|
enabled: true,
|
|
transport: 'arthas-tunnel',
|
|
baseUrl: 'http://127.0.0.1:7777',
|
|
targetId: 'gonavi-local-test',
|
|
apiKey: 'diag-token',
|
|
allowObserveCommands: true,
|
|
allowTraceCommands: true,
|
|
allowMutatingCommands: false,
|
|
timeoutSeconds: 20,
|
|
});
|
|
});
|
|
|
|
it('preserves connection icon metadata when replacing saved connections', async () => {
|
|
const { useStore } = await importStore();
|
|
|
|
useStore.getState().replaceConnections([
|
|
{
|
|
id: 'visual-1',
|
|
name: 'Visual Orders',
|
|
iconType: 'postgres',
|
|
iconColor: '#2f855a',
|
|
config: {
|
|
id: 'visual-1',
|
|
type: 'mysql',
|
|
host: 'db.local',
|
|
port: 3306,
|
|
user: 'root',
|
|
},
|
|
},
|
|
]);
|
|
|
|
expect(useStore.getState().connections[0]?.iconType).toBe('postgres');
|
|
expect(useStore.getState().connections[0]?.iconColor).toBe('#2f855a');
|
|
});
|
|
|
|
it('normalizes ClickHouse protocol override when replacing saved connections', async () => {
|
|
const { useStore } = await importStore();
|
|
|
|
useStore.getState().replaceConnections([
|
|
{
|
|
id: 'clickhouse-http',
|
|
name: 'ClickHouse HTTP',
|
|
config: {
|
|
id: 'clickhouse-http',
|
|
type: 'clickhouse',
|
|
host: 'clickhouse.local',
|
|
port: 8125,
|
|
user: 'default',
|
|
clickHouseProtocol: 'https' as any,
|
|
},
|
|
},
|
|
]);
|
|
|
|
expect(useStore.getState().connections[0]?.config.clickHouseProtocol).toBe(
|
|
'http',
|
|
);
|
|
});
|
|
|
|
it('keeps StarRocks saved connections as independent datasource type', async () => {
|
|
const { useStore } = await importStore();
|
|
|
|
useStore.getState().replaceConnections([
|
|
{
|
|
id: 'starrocks-fe',
|
|
name: 'StarRocks FE',
|
|
config: {
|
|
id: 'starrocks-fe',
|
|
type: 'starrocks',
|
|
host: 'starrocks.local',
|
|
port: 9030,
|
|
user: 'root',
|
|
},
|
|
},
|
|
]);
|
|
|
|
const config = useStore.getState().connections[0]?.config;
|
|
expect(config?.type).toBe('starrocks');
|
|
expect(config?.port).toBe(9030);
|
|
});
|
|
|
|
it('normalizes OceanBase protocol override when replacing saved connections', async () => {
|
|
const { useStore } = await importStore();
|
|
|
|
useStore.getState().replaceConnections([
|
|
{
|
|
id: 'oceanbase-oracle',
|
|
name: 'OceanBase Oracle',
|
|
config: {
|
|
id: 'oceanbase-oracle',
|
|
type: 'oceanbase',
|
|
host: 'ob.local',
|
|
port: 2881,
|
|
user: 'sys@oracle001',
|
|
oceanBaseProtocol: 'oracle',
|
|
},
|
|
},
|
|
]);
|
|
|
|
expect(useStore.getState().connections[0]?.config.oceanBaseProtocol).toBe(
|
|
'oracle',
|
|
);
|
|
});
|
|
|
|
it('restores OceanBase protocol from saved URI or connection params', async () => {
|
|
const { useStore } = await importStore();
|
|
|
|
useStore.getState().replaceConnections([
|
|
{
|
|
id: 'oceanbase-uri-oracle',
|
|
name: 'OceanBase URI Oracle',
|
|
config: {
|
|
id: 'oceanbase-uri-oracle',
|
|
type: 'oceanbase',
|
|
host: 'ob.local',
|
|
port: 2881,
|
|
user: 'sys@oracle001',
|
|
uri: 'oceanbase://sys%40oracle001:pass@ob.local:2881/OBORCL?protocol=oracle',
|
|
},
|
|
},
|
|
{
|
|
id: 'oceanbase-param-oracle',
|
|
name: 'OceanBase Param Oracle',
|
|
config: {
|
|
id: 'oceanbase-param-oracle',
|
|
type: 'oceanbase',
|
|
host: 'ob.local',
|
|
port: 2881,
|
|
user: 'sys@oracle001',
|
|
connectionParams: 'tenantMode=oracle&PREFETCH_ROWS=5000',
|
|
},
|
|
},
|
|
]);
|
|
|
|
expect(useStore.getState().connections[0]?.config.oceanBaseProtocol).toBe(
|
|
'oracle',
|
|
);
|
|
expect(useStore.getState().connections[1]?.config.oceanBaseProtocol).toBe(
|
|
'oracle',
|
|
);
|
|
});
|
|
|
|
it('prefers OceanBase protocol query key over legacy aliases when restoring saved connections', async () => {
|
|
const { useStore } = await importStore();
|
|
|
|
useStore.getState().replaceConnections([
|
|
{
|
|
id: 'oceanbase-conflict',
|
|
name: 'OceanBase Conflict',
|
|
config: {
|
|
id: 'oceanbase-conflict',
|
|
type: 'oceanbase',
|
|
host: 'ob.local',
|
|
port: 2881,
|
|
user: 'root@test',
|
|
connectionParams: 'protocol=mysql&tenantMode=oracle',
|
|
},
|
|
},
|
|
]);
|
|
|
|
expect(useStore.getState().connections[0]?.config.oceanBaseProtocol).toBe(
|
|
'mysql',
|
|
);
|
|
});
|
|
|
|
it('keeps saved OceanBase native protocol loadable for connect-time rejection', async () => {
|
|
const { useStore } = await importStore();
|
|
|
|
expect(() => useStore.getState().replaceConnections([
|
|
{
|
|
id: 'oceanbase-native',
|
|
name: 'OceanBase Native',
|
|
config: {
|
|
id: 'oceanbase-native',
|
|
type: 'oceanbase',
|
|
host: 'ob.local',
|
|
port: 2881,
|
|
user: 'root@test',
|
|
oceanBaseProtocol: 'mysql',
|
|
connectionParams: 'protocol=native',
|
|
},
|
|
},
|
|
])).not.toThrow();
|
|
expect(useStore.getState().connections[0]?.config.connectionParams).toBe(
|
|
'protocol=native',
|
|
);
|
|
expect(useStore.getState().connections[0]?.config.oceanBaseProtocol).toBe(
|
|
'mysql',
|
|
);
|
|
});
|
|
|
|
it('normalizes OceanBase protocol when updating a saved connection', async () => {
|
|
const { useStore } = await importStore();
|
|
|
|
useStore.getState().replaceConnections([
|
|
{
|
|
id: 'oceanbase-existing',
|
|
name: 'OceanBase Existing',
|
|
config: {
|
|
id: 'oceanbase-existing',
|
|
type: 'oceanbase',
|
|
host: 'ob.local',
|
|
port: 2881,
|
|
user: 'root@test',
|
|
connectionParams: 'protocol=mysql',
|
|
},
|
|
},
|
|
]);
|
|
|
|
useStore.getState().updateConnection({
|
|
id: 'oceanbase-existing',
|
|
name: 'OceanBase Existing',
|
|
config: {
|
|
id: 'oceanbase-existing',
|
|
type: 'oceanbase',
|
|
host: 'ob.local',
|
|
port: 2881,
|
|
user: 'sys@oracle001',
|
|
connectionParams: 'protocol=oracle',
|
|
},
|
|
});
|
|
|
|
expect(useStore.getState().connections[0]?.config.oceanBaseProtocol).toBe(
|
|
'oracle',
|
|
);
|
|
});
|
|
|
|
it('keeps legacy global proxy password during hydration until explicit cleanup', async () => {
|
|
storage.setItem('lite-db-storage', JSON.stringify({
|
|
state: {
|
|
globalProxy: {
|
|
enabled: true,
|
|
type: 'http',
|
|
host: '127.0.0.1',
|
|
port: 8080,
|
|
user: 'ops',
|
|
password: 'proxy-secret',
|
|
},
|
|
},
|
|
version: 7,
|
|
}));
|
|
|
|
const { useStore } = await importStore();
|
|
|
|
expect(useStore.getState().globalProxy.password).toBe('proxy-secret');
|
|
expect(useStore.getState().globalProxy.hasPassword).toBe(true);
|
|
});
|
|
|
|
it('persists external SQL directories and restores valid items after reload', async () => {
|
|
const { useStore } = await importStore();
|
|
|
|
useStore.getState().saveExternalSQLDirectory({
|
|
id: 'ext-1',
|
|
name: 'scripts',
|
|
path: 'D:/sql/scripts',
|
|
connectionId: 'conn-1',
|
|
dbName: 'demo',
|
|
createdAt: 1,
|
|
});
|
|
|
|
const persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}');
|
|
expect(persisted.state.externalSQLDirectories).toEqual([
|
|
{
|
|
id: 'ext-1',
|
|
name: 'scripts',
|
|
path: 'D:/sql/scripts',
|
|
connectionId: 'conn-1',
|
|
dbName: 'demo',
|
|
createdAt: 1,
|
|
},
|
|
]);
|
|
|
|
storage.setItem('lite-db-storage', JSON.stringify({
|
|
state: {
|
|
externalSQLDirectories: [
|
|
persisted.state.externalSQLDirectories[0],
|
|
{ path: '', name: 'broken' },
|
|
],
|
|
},
|
|
version: 7,
|
|
}));
|
|
|
|
vi.resetModules();
|
|
const reloaded = await importStore();
|
|
expect(reloaded.useStore.getState().externalSQLDirectories).toEqual([
|
|
{
|
|
id: 'ext-1',
|
|
name: 'scripts',
|
|
path: 'D:/sql/scripts',
|
|
connectionId: 'conn-1',
|
|
dbName: 'demo',
|
|
createdAt: 1,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('defaults AI chat send shortcut to Enter in shared shortcut options', async () => {
|
|
const { useStore } = await importStore();
|
|
|
|
expect(useStore.getState().shortcutOptions.sendAIChatMessage).toEqual({
|
|
combo: 'Enter',
|
|
enabled: true,
|
|
});
|
|
});
|
|
|
|
it('persists recorded AI chat send shortcut and restores it after reload', async () => {
|
|
const { useStore } = await importStore();
|
|
|
|
useStore.getState().updateShortcut('sendAIChatMessage', {
|
|
combo: 'Meta+Enter',
|
|
enabled: true,
|
|
});
|
|
|
|
const persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}');
|
|
expect(persisted.state.shortcutOptions.sendAIChatMessage).toEqual({
|
|
combo: 'Meta+Enter',
|
|
enabled: true,
|
|
});
|
|
|
|
vi.resetModules();
|
|
const reloaded = await importStore();
|
|
expect(reloaded.useStore.getState().shortcutOptions.sendAIChatMessage).toEqual({
|
|
combo: 'Meta+Enter',
|
|
enabled: true,
|
|
});
|
|
});
|
|
|
|
it('falls back to Enter when persisted AI chat send shortcut is invalid', async () => {
|
|
storage.setItem('lite-db-storage', JSON.stringify({
|
|
state: {
|
|
shortcutOptions: {
|
|
sendAIChatMessage: {
|
|
combo: 'A',
|
|
enabled: true,
|
|
},
|
|
},
|
|
},
|
|
version: 8,
|
|
}));
|
|
|
|
const { useStore } = await importStore();
|
|
|
|
expect(useStore.getState().shortcutOptions.sendAIChatMessage).toEqual({
|
|
combo: 'Enter',
|
|
enabled: true,
|
|
});
|
|
});
|
|
|
|
it('does not overwrite recorded AI chat send shortcut during startup config refresh', async () => {
|
|
const { useStore } = await importStore();
|
|
useStore.getState().updateShortcut('sendAIChatMessage', {
|
|
combo: 'Ctrl+Enter',
|
|
enabled: true,
|
|
});
|
|
|
|
useStore.getState().replaceConnections([]);
|
|
|
|
const persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}');
|
|
expect(persisted.state.shortcutOptions.sendAIChatMessage).toEqual({
|
|
combo: 'Ctrl+Enter',
|
|
enabled: true,
|
|
});
|
|
});
|
|
|
|
it('keeps persisted AI chat send shortcut when startup refresh runs before shortcut hydration catches up', async () => {
|
|
const { useStore } = await importStore();
|
|
const shortcutOptions = useStore.getState().shortcutOptions;
|
|
storage.setItem('lite-db-storage', JSON.stringify({
|
|
state: {
|
|
shortcutOptions: {
|
|
...shortcutOptions,
|
|
sendAIChatMessage: {
|
|
combo: 'Meta+Enter',
|
|
enabled: true,
|
|
},
|
|
},
|
|
},
|
|
version: 8,
|
|
}));
|
|
useStore.setState({
|
|
shortcutOptions: {
|
|
...shortcutOptions,
|
|
sendAIChatMessage: {
|
|
combo: 'Enter',
|
|
enabled: true,
|
|
},
|
|
},
|
|
});
|
|
|
|
useStore.getState().replaceConnections([]);
|
|
|
|
const persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}');
|
|
expect(persisted.state.shortcutOptions.sendAIChatMessage).toEqual({
|
|
combo: 'Meta+Enter',
|
|
enabled: true,
|
|
});
|
|
});
|
|
|
|
it('does not let a stale default shortcut state overwrite an explicitly recorded AI chat shortcut', async () => {
|
|
const { useStore } = await importStore();
|
|
const shortcutOptions = useStore.getState().shortcutOptions;
|
|
|
|
useStore.getState().updateShortcut('sendAIChatMessage', {
|
|
combo: 'Meta+Enter',
|
|
enabled: true,
|
|
});
|
|
useStore.setState({
|
|
shortcutOptions: {
|
|
...shortcutOptions,
|
|
sendAIChatMessage: {
|
|
combo: 'Enter',
|
|
enabled: true,
|
|
},
|
|
},
|
|
});
|
|
useStore.getState().replaceGlobalProxy({});
|
|
|
|
const persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}');
|
|
expect(persisted.state.shortcutOptions.sendAIChatMessage).toEqual({
|
|
combo: 'Meta+Enter',
|
|
enabled: true,
|
|
});
|
|
});
|
|
});
|