feat: 添加API运行时健康检查
This commit is contained in:
parent
ce20c350cd
commit
0f47644fd6
@ -1009,50 +1009,77 @@ Add `RuntimeModule` to `imports` after `CommonModule`:
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { RuntimeHealthService } from '../../src/runtime/health/runtime-health.service';
|
import { RuntimeHealthService } from '../../src/runtime/health/runtime-health.service';
|
||||||
|
import type { RuntimeSafeConfigSnapshot } from '../../src/runtime/config/runtime-config.types';
|
||||||
|
|
||||||
|
function createSnapshot(
|
||||||
|
checks: RuntimeSafeConfigSnapshot['checks'],
|
||||||
|
): RuntimeSafeConfigSnapshot {
|
||||||
|
return {
|
||||||
|
app: { nodeEnv: 'test', port: 48085 },
|
||||||
|
database: {
|
||||||
|
host: 'mysql',
|
||||||
|
port: 3306,
|
||||||
|
database: 'kt',
|
||||||
|
username: 'root',
|
||||||
|
synchronize: false,
|
||||||
|
},
|
||||||
|
loki: {
|
||||||
|
transportEnabled: true,
|
||||||
|
httpRequestPushEnabled: true,
|
||||||
|
queryConfigured: true,
|
||||||
|
host: 'https://loki-push.example.test',
|
||||||
|
queryHost: 'https://loki-query.example.test',
|
||||||
|
environment: 'test',
|
||||||
|
tenantId: 'kt',
|
||||||
|
username: 'loki-user',
|
||||||
|
passwordConfigured: true,
|
||||||
|
},
|
||||||
|
minio: {
|
||||||
|
endpoint: 'minio',
|
||||||
|
port: 9000,
|
||||||
|
useSSL: false,
|
||||||
|
accessKey: 'mi***ey',
|
||||||
|
bucket: 'kt-template-online',
|
||||||
|
},
|
||||||
|
wordpress: {
|
||||||
|
baseUrl: 'https://blog.example.test',
|
||||||
|
hostHeader: 'blog.example.test',
|
||||||
|
adminUsername: 'wordpress-admin',
|
||||||
|
passwordConfigured: true,
|
||||||
|
timeoutMs: 15000,
|
||||||
|
loginTimeoutMs: 3000,
|
||||||
|
availabilityTtlMs: 60000,
|
||||||
|
},
|
||||||
|
qqbot: {
|
||||||
|
reverseWsPath: '/qqbot/onebot/reverse',
|
||||||
|
reverseWsToken: 'qq***en',
|
||||||
|
napcatRoot: '/vol1/docker/napcat',
|
||||||
|
napcatContainerMode: 'ssh',
|
||||||
|
napcatSshTarget: 'nas',
|
||||||
|
napcatSshPort: 2202,
|
||||||
|
napcatSshKeyPath: '/home/kt/.ssh/napcat',
|
||||||
|
napcatReverseWsBase: 'ws://api.example.test/onebot',
|
||||||
|
napcatWebuiBaseUrl: 'http://127.0.0.1:6099',
|
||||||
|
napcatWebuiToken: 'na***en',
|
||||||
|
},
|
||||||
|
checks,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createService(snapshot: RuntimeSafeConfigSnapshot) {
|
||||||
|
return new RuntimeHealthService({
|
||||||
|
getSafeSnapshot: jest.fn(() => snapshot),
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
|
||||||
describe('RuntimeHealthService', () => {
|
describe('RuntimeHealthService', () => {
|
||||||
it('returns ready when required config is present and optional config is present', () => {
|
it('returns ready when required and optional checks are present', () => {
|
||||||
const service = new RuntimeHealthService({
|
const service = createService(
|
||||||
getSafeSnapshot: () => ({
|
createSnapshot([
|
||||||
app: { nodeEnv: 'test', port: 48085 },
|
{ key: 'DB_HOST', level: 'required', present: true },
|
||||||
database: {
|
{ key: 'MINIO_ENDPOINT', level: 'optional', present: true },
|
||||||
host: 'mysql',
|
]),
|
||||||
port: 3306,
|
);
|
||||||
database: 'kt',
|
|
||||||
username: 'root',
|
|
||||||
synchronize: false,
|
|
||||||
},
|
|
||||||
loki: {
|
|
||||||
transportEnabled: false,
|
|
||||||
httpRequestPushEnabled: false,
|
|
||||||
queryConfigured: false,
|
|
||||||
host: '',
|
|
||||||
queryHost: '',
|
|
||||||
environment: 'test',
|
|
||||||
tenantId: '',
|
|
||||||
username: '',
|
|
||||||
passwordConfigured: false,
|
|
||||||
},
|
|
||||||
minio: {
|
|
||||||
endpoint: 'minio',
|
|
||||||
port: 9000,
|
|
||||||
useSSL: false,
|
|
||||||
accessKey: 'mi***ey',
|
|
||||||
},
|
|
||||||
wordpress: { endpoint: 'https://example.test', username: 'wo***er' },
|
|
||||||
qqbot: {
|
|
||||||
reverseWsUrl: 'ws://127.0.0.1:3001',
|
|
||||||
napcatDataRoot: '/data/napcat',
|
|
||||||
napcatSshHost: 'nas',
|
|
||||||
napcatSshPort: 22,
|
|
||||||
napcatSshUser: 'root',
|
|
||||||
},
|
|
||||||
checks: [
|
|
||||||
{ key: 'DB_HOST', level: 'required', present: true },
|
|
||||||
{ key: 'MINIO_ENDPOINT', level: 'optional', present: true },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
expect(service.getRuntimeHealth()).toEqual(
|
expect(service.getRuntimeHealth()).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -1063,52 +1090,17 @@ describe('RuntimeHealthService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns blocked when required config is missing', () => {
|
it('returns blocked when required config is missing', () => {
|
||||||
const service = new RuntimeHealthService({
|
const service = createService(
|
||||||
getSafeSnapshot: () => ({
|
createSnapshot([
|
||||||
app: { nodeEnv: 'test', port: 48085 },
|
{
|
||||||
database: {
|
key: 'DB_PASSWORD',
|
||||||
host: '',
|
level: 'required',
|
||||||
port: 3306,
|
present: false,
|
||||||
database: '',
|
message: 'DB_PASSWORD is not configured',
|
||||||
username: '',
|
|
||||||
synchronize: false,
|
|
||||||
},
|
},
|
||||||
loki: {
|
{ key: 'MINIO_ENDPOINT', level: 'optional', present: true },
|
||||||
transportEnabled: false,
|
]),
|
||||||
httpRequestPushEnabled: false,
|
);
|
||||||
queryConfigured: false,
|
|
||||||
host: '',
|
|
||||||
queryHost: '',
|
|
||||||
environment: 'test',
|
|
||||||
tenantId: '',
|
|
||||||
username: '',
|
|
||||||
passwordConfigured: false,
|
|
||||||
},
|
|
||||||
minio: { endpoint: '', port: 9000, useSSL: false, accessKey: '' },
|
|
||||||
wordpress: { endpoint: '', username: '' },
|
|
||||||
qqbot: {
|
|
||||||
reverseWsUrl: '',
|
|
||||||
napcatDataRoot: '',
|
|
||||||
napcatSshHost: '',
|
|
||||||
napcatSshPort: 22,
|
|
||||||
napcatSshUser: '',
|
|
||||||
},
|
|
||||||
checks: [
|
|
||||||
{
|
|
||||||
key: 'DB_PASSWORD',
|
|
||||||
level: 'required',
|
|
||||||
present: false,
|
|
||||||
message: 'DB_PASSWORD is not configured',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'MINIO_ENDPOINT',
|
|
||||||
level: 'optional',
|
|
||||||
present: false,
|
|
||||||
message: 'MINIO_ENDPOINT is not configured',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const report = service.getRuntimeHealth();
|
const report = service.getRuntimeHealth();
|
||||||
|
|
||||||
@ -1121,6 +1113,31 @@ describe('RuntimeHealthService', () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns degraded when only optional config is missing', () => {
|
||||||
|
const service = createService(
|
||||||
|
createSnapshot([
|
||||||
|
{ key: 'DB_HOST', level: 'required', present: true },
|
||||||
|
{
|
||||||
|
key: 'LOKI_PASSWORD',
|
||||||
|
level: 'optional',
|
||||||
|
present: false,
|
||||||
|
message: 'LOKI_PASSWORD is not configured',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const report = service.getRuntimeHealth();
|
||||||
|
|
||||||
|
expect(report.status).toBe('degraded');
|
||||||
|
expect(report.checks).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
name: 'config:LOKI_PASSWORD',
|
||||||
|
status: 'degraded',
|
||||||
|
critical: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -1128,10 +1145,11 @@ describe('RuntimeHealthService', () => {
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { RuntimeHealthController } from '../../src/runtime/health/runtime-health.controller';
|
import { RuntimeHealthController } from '../../src/runtime/health/runtime-health.controller';
|
||||||
|
import type { RuntimeHealthReport } from '../../src/runtime/health/runtime-health.types';
|
||||||
|
|
||||||
describe('RuntimeHealthController', () => {
|
describe('RuntimeHealthController', () => {
|
||||||
it('returns the runtime health report from the service', () => {
|
it('returns the runtime health report from the service', () => {
|
||||||
const report = {
|
const report: RuntimeHealthReport = {
|
||||||
service: 'kt-template-online-api',
|
service: 'kt-template-online-api',
|
||||||
checkedAt: '2026-06-13T00:00:00.000Z',
|
checkedAt: '2026-06-13T00:00:00.000Z',
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
@ -1156,18 +1174,37 @@ describe('RuntimeHealthController', () => {
|
|||||||
username: '',
|
username: '',
|
||||||
passwordConfigured: false,
|
passwordConfigured: false,
|
||||||
},
|
},
|
||||||
minio: { endpoint: '', port: 9000, useSSL: false, accessKey: '' },
|
minio: {
|
||||||
wordpress: { endpoint: '', username: '' },
|
endpoint: 'minio',
|
||||||
|
port: 9000,
|
||||||
|
useSSL: false,
|
||||||
|
accessKey: 'mi***ey',
|
||||||
|
bucket: 'kt-template-online',
|
||||||
|
},
|
||||||
|
wordpress: {
|
||||||
|
baseUrl: 'https://blog.example.test',
|
||||||
|
hostHeader: 'blog.example.test',
|
||||||
|
adminUsername: 'wordpress-admin',
|
||||||
|
passwordConfigured: true,
|
||||||
|
timeoutMs: 15000,
|
||||||
|
loginTimeoutMs: 3000,
|
||||||
|
availabilityTtlMs: 60000,
|
||||||
|
},
|
||||||
qqbot: {
|
qqbot: {
|
||||||
reverseWsUrl: '',
|
reverseWsPath: '/qqbot/onebot/reverse',
|
||||||
napcatDataRoot: '',
|
reverseWsToken: 'qq***en',
|
||||||
napcatSshHost: '',
|
napcatRoot: '/vol1/docker/napcat',
|
||||||
napcatSshPort: 22,
|
napcatContainerMode: 'ssh',
|
||||||
napcatSshUser: '',
|
napcatSshTarget: 'nas',
|
||||||
|
napcatSshPort: 2202,
|
||||||
|
napcatSshKeyPath: '/home/kt/.ssh/napcat',
|
||||||
|
napcatReverseWsBase: 'ws://api.example.test/onebot',
|
||||||
|
napcatWebuiBaseUrl: 'http://127.0.0.1:6099',
|
||||||
|
napcatWebuiToken: 'na***en',
|
||||||
},
|
},
|
||||||
checks: [],
|
checks: [],
|
||||||
},
|
},
|
||||||
} as const;
|
};
|
||||||
const service = { getRuntimeHealth: jest.fn(() => report) };
|
const service = { getRuntimeHealth: jest.fn(() => report) };
|
||||||
const controller = new RuntimeHealthController(service as any);
|
const controller = new RuntimeHealthController(service as any);
|
||||||
|
|
||||||
@ -1193,7 +1230,7 @@ pnpm run typecheck
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
git status --short
|
git status --short
|
||||||
git add src/runtime src/app.module.ts src/main.ts test/runtime
|
git add src/runtime src/app.module.ts src/main.ts test/runtime docs/superpowers/plans/2026-06-13-api-runtime-foundation.md
|
||||||
git diff --cached --check
|
git diff --cached --check
|
||||||
git commit -m "feat: 添加API运行时健康检查"
|
git commit -m "feat: 添加API运行时健康检查"
|
||||||
```
|
```
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import { AdminModule } from './admin/admin.module';
|
|||||||
import { BlogModule } from './blog/blog.module';
|
import { BlogModule } from './blog/blog.module';
|
||||||
import { WordpressModule } from './wordpress/wordpress.module';
|
import { WordpressModule } from './wordpress/wordpress.module';
|
||||||
import { QqbotModule } from './qqbot/qqbot.module';
|
import { QqbotModule } from './qqbot/qqbot.module';
|
||||||
|
import { RuntimeModule } from './runtime';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -64,6 +65,7 @@ import { QqbotModule } from './qqbot/qqbot.module';
|
|||||||
}),
|
}),
|
||||||
MinioClientModule,
|
MinioClientModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
RuntimeModule,
|
||||||
AdminModule,
|
AdminModule,
|
||||||
BlogModule,
|
BlogModule,
|
||||||
WordpressModule,
|
WordpressModule,
|
||||||
|
|||||||
@ -41,7 +41,8 @@ const swaggerGroups: SwaggerDocumentGroup[] = [
|
|||||||
path: 'api/wordpress',
|
path: 'api/wordpress',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
matcher: (path) => path === '/' || path.startsWith('/minio'),
|
matcher: (path) =>
|
||||||
|
path === '/' || path.startsWith('/minio') || path.startsWith('/health'),
|
||||||
name: '基础能力',
|
name: '基础能力',
|
||||||
path: 'api/basic',
|
path: 'api/basic',
|
||||||
},
|
},
|
||||||
|
|||||||
16
src/runtime/health/runtime-health.controller.ts
Normal file
16
src/runtime/health/runtime-health.controller.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { RuntimeHealthService } from './runtime-health.service';
|
||||||
|
import type { RuntimeHealthReport } from './runtime-health.types';
|
||||||
|
|
||||||
|
@ApiTags('Runtime Health')
|
||||||
|
@Controller('health')
|
||||||
|
export class RuntimeHealthController {
|
||||||
|
constructor(private readonly runtimeHealthService: RuntimeHealthService) {}
|
||||||
|
|
||||||
|
@Get('runtime')
|
||||||
|
@ApiOperation({ summary: 'Get machine-readable API runtime health' })
|
||||||
|
getRuntimeHealth(): RuntimeHealthReport {
|
||||||
|
return this.runtimeHealthService.getRuntimeHealth();
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/runtime/health/runtime-health.service.ts
Normal file
64
src/runtime/health/runtime-health.service.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { RuntimeConfigService } from '../config/runtime-config.service';
|
||||||
|
import {
|
||||||
|
RuntimeHealthCheck,
|
||||||
|
RuntimeHealthReport,
|
||||||
|
RuntimeHealthStatus,
|
||||||
|
} from './runtime-health.types';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RuntimeHealthService {
|
||||||
|
constructor(private readonly runtimeConfigService: RuntimeConfigService) {}
|
||||||
|
|
||||||
|
getRuntimeHealth(): RuntimeHealthReport {
|
||||||
|
const config = this.runtimeConfigService.getSafeSnapshot();
|
||||||
|
const checks: RuntimeHealthCheck[] = [
|
||||||
|
{
|
||||||
|
name: 'process',
|
||||||
|
status: 'live',
|
||||||
|
critical: true,
|
||||||
|
message: 'NestJS process answered runtime health request',
|
||||||
|
},
|
||||||
|
...config.checks.map((check) => ({
|
||||||
|
name: `config:${check.key}`,
|
||||||
|
status: this.getConfigCheckStatus(check.present, check.level),
|
||||||
|
critical: check.level === 'required',
|
||||||
|
message: check.present
|
||||||
|
? `${check.key} is configured`
|
||||||
|
: check.message ?? `${check.key} is not configured`,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
service: 'kt-template-online-api',
|
||||||
|
checkedAt: new Date().toISOString(),
|
||||||
|
status: this.aggregateStatus(checks),
|
||||||
|
checks,
|
||||||
|
config,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getConfigCheckStatus(
|
||||||
|
present: boolean,
|
||||||
|
level: 'required' | 'optional',
|
||||||
|
): RuntimeHealthStatus {
|
||||||
|
if (present) return 'ready';
|
||||||
|
return level === 'required' ? 'blocked' : 'degraded';
|
||||||
|
}
|
||||||
|
|
||||||
|
private aggregateStatus(checks: RuntimeHealthCheck[]): RuntimeHealthStatus {
|
||||||
|
if (checks.some((check) => check.critical && check.status === 'blocked')) {
|
||||||
|
return 'blocked';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checks.some((check) => check.status === 'degraded')) {
|
||||||
|
return 'degraded';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checks.every((check) => check.status === 'live')) {
|
||||||
|
return 'live';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'ready';
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/runtime/health/runtime-health.types.ts
Normal file
19
src/runtime/health/runtime-health.types.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import type { RuntimeSafeConfigSnapshot } from '../config/runtime-config.types';
|
||||||
|
|
||||||
|
export type RuntimeHealthStatus = 'live' | 'ready' | 'degraded' | 'blocked';
|
||||||
|
|
||||||
|
export interface RuntimeHealthCheck {
|
||||||
|
name: string;
|
||||||
|
status: RuntimeHealthStatus;
|
||||||
|
critical: boolean;
|
||||||
|
message: string;
|
||||||
|
detail?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuntimeHealthReport {
|
||||||
|
service: 'kt-template-online-api';
|
||||||
|
checkedAt: string;
|
||||||
|
status: RuntimeHealthStatus;
|
||||||
|
checks: RuntimeHealthCheck[];
|
||||||
|
config: RuntimeSafeConfigSnapshot;
|
||||||
|
}
|
||||||
9
src/runtime/index.ts
Normal file
9
src/runtime/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export * from './config/runtime-config.service';
|
||||||
|
export * from './config/runtime-config.types';
|
||||||
|
export * from './errors/runtime-error.types';
|
||||||
|
export * from './evidence/runtime-evidence.service';
|
||||||
|
export * from './evidence/runtime-evidence.types';
|
||||||
|
export * from './health/runtime-health.controller';
|
||||||
|
export * from './health/runtime-health.service';
|
||||||
|
export * from './health/runtime-health.types';
|
||||||
|
export * from './runtime.module';
|
||||||
19
src/runtime/runtime.module.ts
Normal file
19
src/runtime/runtime.module.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { CommonModule } from '../common';
|
||||||
|
import { RuntimeConfigService } from './config/runtime-config.service';
|
||||||
|
import { RuntimeEvidenceService } from './evidence/runtime-evidence.service';
|
||||||
|
import { RuntimeHealthController } from './health/runtime-health.controller';
|
||||||
|
import { RuntimeHealthService } from './health/runtime-health.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [ConfigModule, CommonModule],
|
||||||
|
controllers: [RuntimeHealthController],
|
||||||
|
providers: [
|
||||||
|
RuntimeConfigService,
|
||||||
|
RuntimeEvidenceService,
|
||||||
|
RuntimeHealthService,
|
||||||
|
],
|
||||||
|
exports: [RuntimeConfigService, RuntimeEvidenceService, RuntimeHealthService],
|
||||||
|
})
|
||||||
|
export class RuntimeModule {}
|
||||||
68
test/runtime/runtime-health.controller.spec.ts
Normal file
68
test/runtime/runtime-health.controller.spec.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { RuntimeHealthController } from '../../src/runtime/health/runtime-health.controller';
|
||||||
|
import type { RuntimeHealthReport } from '../../src/runtime/health/runtime-health.types';
|
||||||
|
|
||||||
|
describe('RuntimeHealthController', () => {
|
||||||
|
it('returns the service report and calls the service once', () => {
|
||||||
|
const report: RuntimeHealthReport = {
|
||||||
|
service: 'kt-template-online-api',
|
||||||
|
checkedAt: '2026-06-13T00:00:00.000Z',
|
||||||
|
status: 'ready',
|
||||||
|
checks: [],
|
||||||
|
config: {
|
||||||
|
app: { nodeEnv: 'test', port: 48085 },
|
||||||
|
database: {
|
||||||
|
host: 'mysql',
|
||||||
|
port: 3306,
|
||||||
|
database: 'kt',
|
||||||
|
username: 'root',
|
||||||
|
synchronize: false,
|
||||||
|
},
|
||||||
|
loki: {
|
||||||
|
transportEnabled: false,
|
||||||
|
httpRequestPushEnabled: false,
|
||||||
|
queryConfigured: false,
|
||||||
|
host: '',
|
||||||
|
queryHost: '',
|
||||||
|
environment: 'test',
|
||||||
|
tenantId: '',
|
||||||
|
username: '',
|
||||||
|
passwordConfigured: false,
|
||||||
|
},
|
||||||
|
minio: {
|
||||||
|
endpoint: 'minio',
|
||||||
|
port: 9000,
|
||||||
|
useSSL: false,
|
||||||
|
accessKey: 'mi***ey',
|
||||||
|
bucket: 'kt-template-online',
|
||||||
|
},
|
||||||
|
wordpress: {
|
||||||
|
baseUrl: 'https://blog.example.test',
|
||||||
|
hostHeader: 'blog.example.test',
|
||||||
|
adminUsername: 'wordpress-admin',
|
||||||
|
passwordConfigured: true,
|
||||||
|
timeoutMs: 15000,
|
||||||
|
loginTimeoutMs: 3000,
|
||||||
|
availabilityTtlMs: 60000,
|
||||||
|
},
|
||||||
|
qqbot: {
|
||||||
|
reverseWsPath: '/qqbot/onebot/reverse',
|
||||||
|
reverseWsToken: 'qq***en',
|
||||||
|
napcatRoot: '/vol1/docker/napcat',
|
||||||
|
napcatContainerMode: 'ssh',
|
||||||
|
napcatSshTarget: 'nas',
|
||||||
|
napcatSshPort: 2202,
|
||||||
|
napcatSshKeyPath: '/home/kt/.ssh/napcat',
|
||||||
|
napcatReverseWsBase: 'ws://api.example.test/onebot',
|
||||||
|
napcatWebuiBaseUrl: 'http://127.0.0.1:6099',
|
||||||
|
napcatWebuiToken: 'na***en',
|
||||||
|
},
|
||||||
|
checks: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const service = { getRuntimeHealth: jest.fn(() => report) };
|
||||||
|
const controller = new RuntimeHealthController(service as any);
|
||||||
|
|
||||||
|
expect(controller.getRuntimeHealth()).toBe(report);
|
||||||
|
expect(service.getRuntimeHealth).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
161
test/runtime/runtime-health.service.spec.ts
Normal file
161
test/runtime/runtime-health.service.spec.ts
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import { RuntimeHealthService } from '../../src/runtime/health/runtime-health.service';
|
||||||
|
import type { RuntimeSafeConfigSnapshot } from '../../src/runtime/config/runtime-config.types';
|
||||||
|
|
||||||
|
function createSnapshot(
|
||||||
|
checks: RuntimeSafeConfigSnapshot['checks'],
|
||||||
|
): RuntimeSafeConfigSnapshot {
|
||||||
|
return {
|
||||||
|
app: { nodeEnv: 'test', port: 48085 },
|
||||||
|
database: {
|
||||||
|
host: 'mysql',
|
||||||
|
port: 3306,
|
||||||
|
database: 'kt',
|
||||||
|
username: 'root',
|
||||||
|
synchronize: false,
|
||||||
|
},
|
||||||
|
loki: {
|
||||||
|
transportEnabled: true,
|
||||||
|
httpRequestPushEnabled: true,
|
||||||
|
queryConfigured: true,
|
||||||
|
host: 'https://loki-push.example.test',
|
||||||
|
queryHost: 'https://loki-query.example.test',
|
||||||
|
environment: 'test',
|
||||||
|
tenantId: 'kt',
|
||||||
|
username: 'loki-user',
|
||||||
|
passwordConfigured: true,
|
||||||
|
},
|
||||||
|
minio: {
|
||||||
|
endpoint: 'minio',
|
||||||
|
port: 9000,
|
||||||
|
useSSL: false,
|
||||||
|
accessKey: 'mi***ey',
|
||||||
|
bucket: 'kt-template-online',
|
||||||
|
},
|
||||||
|
wordpress: {
|
||||||
|
baseUrl: 'https://blog.example.test',
|
||||||
|
hostHeader: 'blog.example.test',
|
||||||
|
adminUsername: 'wordpress-admin',
|
||||||
|
passwordConfigured: true,
|
||||||
|
timeoutMs: 15000,
|
||||||
|
loginTimeoutMs: 3000,
|
||||||
|
availabilityTtlMs: 60000,
|
||||||
|
},
|
||||||
|
qqbot: {
|
||||||
|
reverseWsPath: '/qqbot/onebot/reverse',
|
||||||
|
reverseWsToken: 'qq***en',
|
||||||
|
napcatRoot: '/vol1/docker/napcat',
|
||||||
|
napcatContainerMode: 'ssh',
|
||||||
|
napcatSshTarget: 'nas',
|
||||||
|
napcatSshPort: 2202,
|
||||||
|
napcatSshKeyPath: '/home/kt/.ssh/napcat',
|
||||||
|
napcatReverseWsBase: 'ws://api.example.test/onebot',
|
||||||
|
napcatWebuiBaseUrl: 'http://127.0.0.1:6099',
|
||||||
|
napcatWebuiToken: 'na***en',
|
||||||
|
},
|
||||||
|
checks,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createService(snapshot: RuntimeSafeConfigSnapshot) {
|
||||||
|
return new RuntimeHealthService({
|
||||||
|
getSafeSnapshot: jest.fn(() => snapshot),
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('RuntimeHealthService', () => {
|
||||||
|
it('returns ready when required and optional checks are present', () => {
|
||||||
|
const service = createService(
|
||||||
|
createSnapshot([
|
||||||
|
{ key: 'DB_HOST', level: 'required', present: true },
|
||||||
|
{ key: 'MINIO_ENDPOINT', level: 'optional', present: true },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const report = service.getRuntimeHealth();
|
||||||
|
|
||||||
|
expect(report).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
service: 'kt-template-online-api',
|
||||||
|
status: 'ready',
|
||||||
|
config: expect.objectContaining({
|
||||||
|
minio: expect.objectContaining({ bucket: 'kt-template-online' }),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(new Date(report.checkedAt).toISOString()).toBe(report.checkedAt);
|
||||||
|
expect(report.checks).toContainEqual({
|
||||||
|
name: 'process',
|
||||||
|
status: 'live',
|
||||||
|
critical: true,
|
||||||
|
message: 'NestJS process answered runtime health request',
|
||||||
|
});
|
||||||
|
expect(report.checks).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
name: 'config:DB_HOST',
|
||||||
|
status: 'ready',
|
||||||
|
critical: true,
|
||||||
|
message: 'DB_HOST is configured',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(report.checks).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
name: 'config:MINIO_ENDPOINT',
|
||||||
|
status: 'ready',
|
||||||
|
critical: false,
|
||||||
|
message: 'MINIO_ENDPOINT is configured',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns blocked when required config is missing', () => {
|
||||||
|
const service = createService(
|
||||||
|
createSnapshot([
|
||||||
|
{
|
||||||
|
key: 'DB_PASSWORD',
|
||||||
|
level: 'required',
|
||||||
|
present: false,
|
||||||
|
message: 'DB_PASSWORD is not configured',
|
||||||
|
},
|
||||||
|
{ key: 'MINIO_ENDPOINT', level: 'optional', present: true },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const report = service.getRuntimeHealth();
|
||||||
|
|
||||||
|
expect(report.status).toBe('blocked');
|
||||||
|
expect(report.checks).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
name: 'config:DB_PASSWORD',
|
||||||
|
status: 'blocked',
|
||||||
|
critical: true,
|
||||||
|
message: 'DB_PASSWORD is not configured',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns degraded when only optional config is missing', () => {
|
||||||
|
const service = createService(
|
||||||
|
createSnapshot([
|
||||||
|
{ key: 'DB_HOST', level: 'required', present: true },
|
||||||
|
{
|
||||||
|
key: 'LOKI_PASSWORD',
|
||||||
|
level: 'optional',
|
||||||
|
present: false,
|
||||||
|
message: 'LOKI_PASSWORD is not configured',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const report = service.getRuntimeHealth();
|
||||||
|
|
||||||
|
expect(report.status).toBe('degraded');
|
||||||
|
expect(report.checks).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
name: 'config:LOKI_PASSWORD',
|
||||||
|
status: 'degraded',
|
||||||
|
critical: false,
|
||||||
|
message: 'LOKI_PASSWORD is not configured',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user