feat: M-AI-03 Synthetic Job Definition + 环境门控 + 测试
- synthetic-job-definition.ts: SyntheticDefinition + isSyntheticEnabled() + assertNotProduction() - synthetic-registration.service.ts: onModuleInit 条件注册(仅 test+SYNTHETIC_ENABLED) - synthetic-job-definition.spec.ts: 13 测试(环境门控/Definition 合法性/Registry) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c91a290536
commit
2cf9579935
@ -15,6 +15,7 @@ import { RESULT_PROJECTORS } from './result-projector.interface';
|
||||
import { AiJobController } from './ai-job.controller';
|
||||
import { AiJobAdminController } from './ai-job-admin.controller';
|
||||
import { AiJobService } from './ai-job.service';
|
||||
import { SyntheticRegistrationService } from './synthetic-registration.service';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, AiRuntimeModule, AiModule],
|
||||
@ -28,6 +29,7 @@ import { AiJobService } from './ai-job.service';
|
||||
AiJobService,
|
||||
AiJobExecutionEngineImpl,
|
||||
ProjectionExecutor,
|
||||
SyntheticRegistrationService,
|
||||
{ provide: RESULT_PROJECTORS, useFactory: (p: SyntheticResultProjector) => p, inject: [SyntheticResultProjector], multi: true } as any,
|
||||
{ provide: AI_JOB_EXECUTION_ENGINE, useExisting: AiJobExecutionEngineImpl },
|
||||
],
|
||||
|
||||
95
src/modules/ai-job/synthetic-job-definition.spec.ts
Normal file
95
src/modules/ai-job/synthetic-job-definition.spec.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import {
|
||||
SYNTHETIC_JOB_DEFINITION,
|
||||
isSyntheticEnabled,
|
||||
assertNotProduction,
|
||||
} from './synthetic-job-definition';
|
||||
import { JobDefinitionRegistry, DuplicateJobTypeError } from './job-definition-registry';
|
||||
import { ALLOWED_QUEUE_NAMES } from './job-definition.types';
|
||||
|
||||
describe('Synthetic Job Definition', () => {
|
||||
const OLD_ENV = { ...process.env };
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...OLD_ENV };
|
||||
});
|
||||
|
||||
describe('环境门控', () => {
|
||||
it('NODE_ENV=test + AI_JOB_SYNTHETIC_ENABLED=true → isSyntheticEnabled=true', () => {
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.AI_JOB_SYNTHETIC_ENABLED = 'true';
|
||||
expect(isSyntheticEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
it('NODE_ENV=production → isSyntheticEnabled=false', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
process.env.AI_JOB_SYNTHETIC_ENABLED = 'true';
|
||||
expect(isSyntheticEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('AI_JOB_SYNTHETIC_ENABLED 未设置 → isSyntheticEnabled=false', () => {
|
||||
process.env.NODE_ENV = 'test';
|
||||
delete process.env.AI_JOB_SYNTHETIC_ENABLED;
|
||||
expect(isSyntheticEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('生产环境 assertNotProduction 抛出错误', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
expect(() => assertNotProduction(SYNTHETIC_JOB_DEFINITION)).toThrow(
|
||||
'Synthetic Job Definition detected in production',
|
||||
);
|
||||
});
|
||||
|
||||
it('测试环境 assertNotProduction 不抛出', () => {
|
||||
process.env.NODE_ENV = 'test';
|
||||
expect(() => assertNotProduction(SYNTHETIC_JOB_DEFINITION)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Definition 合法性', () => {
|
||||
let registry: JobDefinitionRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
registry = new JobDefinitionRegistry();
|
||||
});
|
||||
|
||||
it('Synthetic Definition 可通过 Registry 注册', () => {
|
||||
registry.register(SYNTHETIC_JOB_DEFINITION);
|
||||
expect(registry.get('synthetic_job')).toBeDefined();
|
||||
expect(registry.has('synthetic_job')).toBe(true);
|
||||
});
|
||||
|
||||
it('重复注册抛出 DuplicateJobTypeError', () => {
|
||||
registry.register(SYNTHETIC_JOB_DEFINITION);
|
||||
expect(() => registry.register(SYNTHETIC_JOB_DEFINITION)).toThrow(
|
||||
DuplicateJobTypeError,
|
||||
);
|
||||
});
|
||||
|
||||
it('queueName 在允许列表中', () => {
|
||||
expect(ALLOWED_QUEUE_NAMES).toContain(SYNTHETIC_JOB_DEFINITION.queue.queueName);
|
||||
});
|
||||
|
||||
it('timeoutMs 在合法范围 [1000, 600000]', () => {
|
||||
expect(SYNTHETIC_JOB_DEFINITION.execution.timeoutMs).toBeGreaterThanOrEqual(1000);
|
||||
expect(SYNTHETIC_JOB_DEFINITION.execution.timeoutMs).toBeLessThanOrEqual(600_000);
|
||||
});
|
||||
|
||||
it('maxRetries 在合法范围 [0, 10]', () => {
|
||||
expect(SYNTHETIC_JOB_DEFINITION.execution.maxRetries).toBeGreaterThanOrEqual(0);
|
||||
expect(SYNTHETIC_JOB_DEFINITION.execution.maxRetries).toBeLessThanOrEqual(10);
|
||||
});
|
||||
|
||||
it('input/output schema non-empty', () => {
|
||||
expect(SYNTHETIC_JOB_DEFINITION.input.schemaVersion).toBeTruthy();
|
||||
expect(SYNTHETIC_JOB_DEFINITION.output.schemaVersion).toBeTruthy();
|
||||
});
|
||||
|
||||
it('projectorKey 指向 synthetic_projector', () => {
|
||||
expect(SYNTHETIC_JOB_DEFINITION.projectorKey).toBe('synthetic_projector');
|
||||
});
|
||||
|
||||
it('cancellable=true(测试取消场景需要)', () => {
|
||||
expect(SYNTHETIC_JOB_DEFINITION.execution.cancellable).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
71
src/modules/ai-job/synthetic-job-definition.ts
Normal file
71
src/modules/ai-job/synthetic-job-definition.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import type { JobDefinition } from './job-definition.types';
|
||||
|
||||
/**
|
||||
* M-AI-03-011: Synthetic Job Definition(仅测试环境注册)
|
||||
*
|
||||
* 验证完整 Engine 链路而不依赖真实业务 Job Definition。
|
||||
*
|
||||
* 注册条件:NODE_ENV=test && AI_JOB_SYNTHETIC_ENABLED=true
|
||||
* 生产环境检测到该 Definition → 抛出异常阻止启动。
|
||||
*/
|
||||
|
||||
export const SYNTHETIC_JOB_DEFINITION: JobDefinition = {
|
||||
jobType: 'synthetic_job',
|
||||
metadata: {
|
||||
label: 'Synthetic E2E Test Job',
|
||||
description: 'Synthetic job for M-AI-03 end-to-end validation. Registered only in test environment.',
|
||||
domain: 'analysis',
|
||||
version: '1.0.0',
|
||||
},
|
||||
queue: {
|
||||
queueName: 'ai-interactive',
|
||||
defaultPriority: 0,
|
||||
},
|
||||
execution: {
|
||||
timeoutMs: 10_000, // 10s for test
|
||||
maxRetries: 1, // 1 retry for quick failure detection
|
||||
retryBackoff: { type: 'exponential', delay: 1000 },
|
||||
cancellable: true,
|
||||
abortStrategy: 'fail',
|
||||
},
|
||||
input: {
|
||||
schemaVersion: 'synthetic-1.0',
|
||||
},
|
||||
output: {
|
||||
schemaVersion: 'synthetic-1.0',
|
||||
},
|
||||
prompt: {
|
||||
promptKey: 'synthetic_test',
|
||||
promptVersion: '1.0',
|
||||
},
|
||||
model: {
|
||||
modelTier: 'primary',
|
||||
modelProvider: 'mock',
|
||||
modelName: 'mock-provider',
|
||||
maxTokens: 100,
|
||||
},
|
||||
credential: {
|
||||
allowedModes: ['platform_key'] as const,
|
||||
defaultMode: 'platform_key',
|
||||
},
|
||||
projectorKey: 'synthetic_projector',
|
||||
security: {
|
||||
contentSafetyCheck: false,
|
||||
outputRedaction: false,
|
||||
},
|
||||
};
|
||||
|
||||
/** 检查是否允许注册 Synthetic Definition */
|
||||
export function isSyntheticEnabled(): boolean {
|
||||
return process.env.NODE_ENV === 'test' && process.env.AI_JOB_SYNTHETIC_ENABLED === 'true';
|
||||
}
|
||||
|
||||
/** 生产环境保护:若检测到 Synthetic Definition 注册尝试,启动失败 */
|
||||
export function assertNotProduction(def: JobDefinition): void {
|
||||
if (def.jobType === 'synthetic_job' && process.env.NODE_ENV === 'production') {
|
||||
throw new Error(
|
||||
'Synthetic Job Definition detected in production environment! ' +
|
||||
'This is a test-only artifact and must not be registered outside test environments.',
|
||||
);
|
||||
}
|
||||
}
|
||||
36
src/modules/ai-job/synthetic-registration.service.ts
Normal file
36
src/modules/ai-job/synthetic-registration.service.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { JobDefinitionRegistry } from './job-definition-registry';
|
||||
import {
|
||||
SYNTHETIC_JOB_DEFINITION,
|
||||
isSyntheticEnabled,
|
||||
assertNotProduction,
|
||||
} from './synthetic-job-definition';
|
||||
|
||||
/**
|
||||
* M-AI-03-011: Synthetic Definition 注册服务
|
||||
*
|
||||
* 仅在 NODE_ENV=test && AI_JOB_SYNTHETIC_ENABLED=true 时注册。
|
||||
* 生产环境检测到 → 抛出异常阻止启动。
|
||||
*/
|
||||
@Injectable()
|
||||
export class SyntheticRegistrationService implements OnModuleInit {
|
||||
private readonly logger = new Logger(SyntheticRegistrationService.name);
|
||||
|
||||
constructor(private readonly registry: JobDefinitionRegistry) {}
|
||||
|
||||
onModuleInit(): void {
|
||||
if (isSyntheticEnabled()) {
|
||||
assertNotProduction(SYNTHETIC_JOB_DEFINITION);
|
||||
this.registry.register(SYNTHETIC_JOB_DEFINITION);
|
||||
this.logger.log(
|
||||
`Synthetic Job Definition registered: jobType="${SYNTHETIC_JOB_DEFINITION.jobType}" ` +
|
||||
`queue="${SYNTHETIC_JOB_DEFINITION.queue.queueName}"`,
|
||||
);
|
||||
} else {
|
||||
this.logger.log(
|
||||
`Synthetic Job Definition NOT registered ` +
|
||||
`(NODE_ENV=${process.env.NODE_ENV}, SYNTHETIC_ENABLED=${process.env.AI_JOB_SYNTHETIC_ENABLED ?? 'unset'})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user