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 { AiJobController } from './ai-job.controller';
|
||||||
import { AiJobAdminController } from './ai-job-admin.controller';
|
import { AiJobAdminController } from './ai-job-admin.controller';
|
||||||
import { AiJobService } from './ai-job.service';
|
import { AiJobService } from './ai-job.service';
|
||||||
|
import { SyntheticRegistrationService } from './synthetic-registration.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, AiRuntimeModule, AiModule],
|
imports: [PrismaModule, AiRuntimeModule, AiModule],
|
||||||
@ -28,6 +29,7 @@ import { AiJobService } from './ai-job.service';
|
|||||||
AiJobService,
|
AiJobService,
|
||||||
AiJobExecutionEngineImpl,
|
AiJobExecutionEngineImpl,
|
||||||
ProjectionExecutor,
|
ProjectionExecutor,
|
||||||
|
SyntheticRegistrationService,
|
||||||
{ provide: RESULT_PROJECTORS, useFactory: (p: SyntheticResultProjector) => p, inject: [SyntheticResultProjector], multi: true } as any,
|
{ provide: RESULT_PROJECTORS, useFactory: (p: SyntheticResultProjector) => p, inject: [SyntheticResultProjector], multi: true } as any,
|
||||||
{ provide: AI_JOB_EXECUTION_ENGINE, useExisting: AiJobExecutionEngineImpl },
|
{ 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