feat: M-AI-03 Synthetic Job Definition + 环境门控 + 测试
Some checks failed
Deploy API Server / build-and-unit (push) Failing after 28s
Deploy API Server / current-integration (push) Has been skipped
Deploy API Server / backward-compat (push) Has been skipped
Deploy API Server / deploy (push) Has been skipped

- 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:
wangdl 2026-06-21 10:11:16 +08:00
parent c91a290536
commit 2cf9579935
4 changed files with 204 additions and 0 deletions

View File

@ -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 },
], ],

View 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);
});
});
});

View 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.',
);
}
}

View 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'})`,
);
}
}
}