From 2cf9579935bffbf03fd44ab619ed3bbd4bd0f78f Mon Sep 17 00:00:00 2001 From: wangdl Date: Sun, 21 Jun 2026 10:11:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20M-AI-03=20Synthetic=20Job=20Definition?= =?UTF-8?q?=20+=20=E7=8E=AF=E5=A2=83=E9=97=A8=E6=8E=A7=20+=20=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/modules/ai-job/ai-job.module.ts | 2 + .../ai-job/synthetic-job-definition.spec.ts | 95 +++++++++++++++++++ .../ai-job/synthetic-job-definition.ts | 71 ++++++++++++++ .../ai-job/synthetic-registration.service.ts | 36 +++++++ 4 files changed, 204 insertions(+) create mode 100644 src/modules/ai-job/synthetic-job-definition.spec.ts create mode 100644 src/modules/ai-job/synthetic-job-definition.ts create mode 100644 src/modules/ai-job/synthetic-registration.service.ts diff --git a/src/modules/ai-job/ai-job.module.ts b/src/modules/ai-job/ai-job.module.ts index f305945..d7f5147 100644 --- a/src/modules/ai-job/ai-job.module.ts +++ b/src/modules/ai-job/ai-job.module.ts @@ -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 }, ], diff --git a/src/modules/ai-job/synthetic-job-definition.spec.ts b/src/modules/ai-job/synthetic-job-definition.spec.ts new file mode 100644 index 0000000..2d2eb6e --- /dev/null +++ b/src/modules/ai-job/synthetic-job-definition.spec.ts @@ -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); + }); + }); +}); diff --git a/src/modules/ai-job/synthetic-job-definition.ts b/src/modules/ai-job/synthetic-job-definition.ts new file mode 100644 index 0000000..e166813 --- /dev/null +++ b/src/modules/ai-job/synthetic-job-definition.ts @@ -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.', + ); + } +} diff --git a/src/modules/ai-job/synthetic-registration.service.ts b/src/modules/ai-job/synthetic-registration.service.ts new file mode 100644 index 0000000..753507d --- /dev/null +++ b/src/modules/ai-job/synthetic-registration.service.ts @@ -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'})`, + ); + } + } +}