feat(jvm): 落地 JVM 连接契约与配置归一化

- 新增 JVM 连接配置与共享 DTO,补齐 JMX 和 Endpoint 契约
- 实现后端归一化规则,支持默认只读、模式回退和 JMX 端口兜底
- 新增前端 JVM 默认值与配置构建工具,统一模式环境和端口收敛
- 补充 Go 与 Vitest 用例并更新需求追踪,记录 Task 1 验证证据
This commit is contained in:
Syngnat
2026-04-22 17:20:00 +08:00
parent f584270209
commit 15b1ad24d1
8 changed files with 549 additions and 5 deletions

View File

@@ -0,0 +1,61 @@
import { describe, expect, it } from 'vitest';
import { buildDefaultJVMConnectionValues, buildJVMConnectionConfig } from './jvmConnectionConfig';
describe('jvmConnectionConfig', () => {
it('defaults to readonly jmx mode', () => {
const values = buildDefaultJVMConnectionValues();
expect(values.type).toBe('jvm');
expect(values.jvmReadOnly).toBe(true);
expect(values.jvmAllowedModes).toEqual(['jmx']);
expect(values.jvmPreferredMode).toBe('jmx');
});
it('builds nested jvm config payload', () => {
const config = buildJVMConnectionConfig({
name: 'Orders JVM',
type: 'jvm',
host: 'orders.internal',
port: 9010,
jvmReadOnly: true,
jvmAllowedModes: ['jmx', 'endpoint'],
jvmPreferredMode: 'endpoint',
jvmEnvironment: 'prod',
jvmEndpointEnabled: true,
jvmEndpointBaseUrl: 'https://orders.internal/manage/jvm',
jvmEndpointApiKey: 'token-1',
});
expect(config.jvm?.preferredMode).toBe('endpoint');
expect(config.jvm?.endpoint?.baseUrl).toBe('https://orders.internal/manage/jvm');
});
it('normalizes allowed modes and falls back preferred mode to first allowed mode', () => {
const config = buildJVMConnectionConfig({
host: 'cache.internal',
port: 9010,
jvmAllowedModes: [' Endpoint ', 'invalid', 'JMX', 'endpoint'],
jvmPreferredMode: 'AGENT',
});
expect(config.jvm?.allowedModes).toEqual(['endpoint', 'jmx']);
expect(config.jvm?.preferredMode).toBe('endpoint');
expect(config.jvm?.jmx?.enabled).toBe(true);
});
it('normalizes environment and port defaults when input is invalid', () => {
const config = buildJVMConnectionConfig({
host: 'orders.internal',
port: 0,
jvmJmxPort: '',
jvmEnvironment: ' PROD ',
jvmReadOnly: false,
jvmAllowedModes: ['JMX'],
jvmPreferredMode: 'jmx',
});
expect(config.port).toBe(9010);
expect(config.jvm?.jmx?.port).toBe(9010);
expect(config.jvm?.environment).toBe('prod');
expect(config.jvm?.readOnly).toBe(false);
});
});

View File

@@ -0,0 +1,123 @@
import type { ConnectionConfig } from '../types';
const DEFAULT_JMX_PORT = 9010;
const DEFAULT_TIMEOUT_SECONDS = 30;
const DEFAULT_ENVIRONMENT = 'dev';
const JVM_MODES = ['jmx', 'endpoint', 'agent'] as const;
type JVMMode = typeof JVM_MODES[number];
type JVMEnvironment = 'dev' | 'uat' | 'prod';
type JVMConnectionFormValues = Record<string, unknown>;
const isJVMMode = (value: string): value is JVMMode => JVM_MODES.includes(value as JVMMode);
const toStringValue = (value: unknown): string => {
if (typeof value === 'string') {
return value.trim();
}
if (typeof value === 'number' || typeof value === 'boolean') {
return String(value).trim();
}
return '';
};
const toInteger = (value: unknown, fallback: number): number => {
if (value === undefined || value === null || value === '') {
return fallback;
}
const parsed = Number(value);
if (!Number.isFinite(parsed)) {
return fallback;
}
const intValue = Math.trunc(parsed);
return intValue > 0 ? intValue : fallback;
};
const normalizeModes = (value: unknown): JVMMode[] => {
if (!Array.isArray(value)) {
return ['jmx'];
}
const result: JVMMode[] = [];
const seen = new Set<JVMMode>();
for (const item of value) {
const mode = toStringValue(item).toLowerCase();
if (!isJVMMode(mode) || seen.has(mode)) {
continue;
}
seen.add(mode);
result.push(mode);
}
return result.length > 0 ? result : ['jmx'];
};
const normalizePreferredMode = (value: unknown, allowedModes: JVMMode[]): JVMMode => {
const preferred = toStringValue(value).toLowerCase();
if (isJVMMode(preferred) && allowedModes.includes(preferred)) {
return preferred;
}
return allowedModes[0];
};
const normalizeEnvironment = (value: unknown): JVMEnvironment => {
const env = toStringValue(value).toLowerCase();
if (env === 'uat' || env === 'prod') {
return env;
}
return DEFAULT_ENVIRONMENT;
};
const normalizeReadOnly = (value: unknown): boolean => {
if (typeof value === 'boolean') {
return value;
}
return true;
};
export const buildDefaultJVMConnectionValues = () => ({
type: 'jvm',
host: 'localhost',
port: DEFAULT_JMX_PORT,
jvmReadOnly: true,
jvmAllowedModes: ['jmx'],
jvmPreferredMode: 'jmx',
jvmEnvironment: DEFAULT_ENVIRONMENT,
jvmEndpointEnabled: false,
jvmEndpointBaseUrl: '',
jvmEndpointApiKey: '',
});
export const buildJVMConnectionConfig = (values: JVMConnectionFormValues): ConnectionConfig => {
const allowedModes = normalizeModes(values.jvmAllowedModes);
const preferredMode = normalizePreferredMode(values.jvmPreferredMode, allowedModes);
const port = toInteger(values.port, DEFAULT_JMX_PORT);
const timeout = toInteger(values.timeout, DEFAULT_TIMEOUT_SECONDS);
return {
type: 'jvm',
host: toStringValue(values.host),
port,
user: '',
password: '',
timeout,
jvm: {
environment: normalizeEnvironment(values.jvmEnvironment),
readOnly: normalizeReadOnly(values.jvmReadOnly),
allowedModes,
preferredMode,
jmx: {
enabled: allowedModes.includes('jmx'),
host: toStringValue(values.jvmJmxHost) || toStringValue(values.host),
port: toInteger(values.jvmJmxPort, port),
username: toStringValue(values.jvmJmxUsername),
password: toStringValue(values.jvmJmxPassword),
},
endpoint: {
enabled: values.jvmEndpointEnabled === true,
baseUrl: toStringValue(values.jvmEndpointBaseUrl),
apiKey: toStringValue(values.jvmEndpointApiKey),
timeoutSeconds: toInteger(values.jvmEndpointTimeoutSeconds, timeout),
},
},
};
};