api-server/test/m-ai-03-gate.e2e-spec.ts
wangdl 817b17bf9e
Some checks failed
Deploy API Server / build-and-unit (push) Successful in 34s
Deploy API Server / current-integration (push) Successful in 30s
Deploy API Server / backward-compat (push) Successful in 1s
Deploy API Server / m-ai-03-synthetic-e2e (push) Failing after 11s
Deploy API Server / deploy (push) Has been skipped
fix: Gate E2E — NestFactory.create 替代 Test.createTestingModule
Test.createTestingModule 不提供全局 Reflector 给子模块 AppConfigModule,
导致 AdminAuthGuard 依赖解析失败。
NestFactory.create 正确初始化所有 @nestjs/core 全局 providers。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-21 11:43:53 +08:00

186 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { NestFactory } from '@nestjs/core';
import { INestApplication } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import request from 'supertest';
import { AppModule } from '../src/app.module';
import { AiJobCreationService } from '../src/modules/ai-job/ai-job-creation.service';
import { JobDefinitionRegistry } from '../src/modules/ai-job/job-definition-registry';
import { AiJobLifecycleRepository } from '../src/modules/ai-job/ai-job-lifecycle.repository';
/**
* M-AI-03 GATE E2E — Fail-Closed 核心门禁
*
* REQUIRE_REAL_INFRA=true 时:
* AppModule.init() → Prisma/Redis 连接 → 成功则 infra OK
* 连接失败 → beforeAll 抛错 → 测试失败fail-closed
*/
const OLD_ENV = { ...process.env };
async function assertGateEnv(): Promise<void> {
if (process.env.REQUIRE_REAL_INFRA !== 'true') {
throw new Error('REQUIRE_REAL_INFRA must be "true" to run M-AI-03 Gate E2E');
}
}
const TEST_USER = 'gate-e2e-user';
describe('M-AI-03 GATE E2E (fail-closed)', () => {
let app: INestApplication;
let creationService: AiJobCreationService;
let registry: JobDefinitionRegistry;
let lifecycleRepo: AiJobLifecycleRepository;
let jwtService: JwtService;
let userToken: string;
beforeAll(async () => {
await assertGateEnv();
process.env.NODE_ENV = 'test';
process.env.AI_JOB_SYNTHETIC_ENABLED = 'true';
process.env.JWT_SECRET = 'gate-e2e-secret';
// NestFactory.create 提供所有 core providers (Reflector etc.)
// 连接 Prisma + Redis — 失败则测试 fail-closed
app = await NestFactory.create(AppModule);
await app.init();
creationService = app.get(AiJobCreationService);
registry = app.get(JobDefinitionRegistry);
lifecycleRepo = app.get(AiJobLifecycleRepository);
jwtService = app.get(JwtService);
userToken = jwtService.sign({ sub: TEST_USER, email: 'gate@test.com', role: 'USER', type: 'user' });
}, 30000);
afterAll(async () => {
process.env = OLD_ENV;
if (app) await app.close();
});
// ═══════════════ 场景 1: 完整成功链路 ═══════════════
it('1. synthetic_job 创建 → snapshot 存在 → outbox 存在', async () => {
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
expect(registry.has('synthetic_job')).toBe(true);
const job = await creationService.createJob({
userId: TEST_USER,
jobType: 'synthetic_job',
triggerType: 'user_api',
targetType: 'synthetic',
targetId: `gt-${Date.now()}`,
idempotencyKey: `gt-s1-${Date.now()}`,
});
expect(job).toBeDefined();
expect(job.lifecycleStatus).toBe('queued');
expect(job.queueName).toBe('ai-interactive');
// Snapshot
const snap = await prisma.aiJobSnapshot.findUnique({ where: { jobId: job.id } });
expect(snap).toBeDefined();
expect(snap.contentHash).toBeTruthy();
// Outbox: payload = {jobId} only
const outbox = await prisma.outboxEvent.findFirst({ where: { aggregateId: job.id } });
expect(outbox).toBeDefined();
expect(outbox.eventType).toBe('ai.job.enqueue');
expect(outbox.payload).toEqual({ jobId: job.id });
await prisma.$disconnect();
});
// ═══════════════ 场景 2: Admin Retry 请求级幂等 ═══════════════
it('2. Admin Retry: 同 operationId 返回同一 Child Job; 不同 operationId 创建不同 Job', async () => {
const idemKey = `gt-retry-src-${Date.now()}`;
const original = await creationService.createJob({
userId: TEST_USER,
jobType: 'synthetic_job',
triggerType: 'user_api',
targetType: 'synthetic',
targetId: `rt-${Date.now()}`,
idempotencyKey: idemKey,
});
await lifecycleRepo.markCancelled(original.id);
const adminToken = jwtService.sign({ sub: 'admin-gate', email: 'ag@t.com', role: 'SUPER_ADMIN', type: 'admin' });
const opId = `gt-op-${Date.now()}`;
// 同 operationId 两次 → 同一 Child Job
const res1 = await request(app.getHttpServer())
.post(`/admin-api/ai/jobs/${original.id}/retry?operationId=${opId}`)
.set('Authorization', `Bearer ${adminToken}`).expect(200);
const res2 = await request(app.getHttpServer())
.post(`/admin-api/ai/jobs/${original.id}/retry?operationId=${opId}`)
.set('Authorization', `Bearer ${adminToken}`).expect(200);
expect(res2.body.id).toBe(res1.body.id);
expect(res2.body.parentJobId).toBe(original.id);
expect(res2.body.triggerType).toBe('admin_manual');
// 不同 operationId → 不同 Child Job
const res3 = await request(app.getHttpServer())
.post(`/admin-api/ai/jobs/${original.id}/retry?operationId=gt-op-other-${Date.now()}`)
.set('Authorization', `Bearer ${adminToken}`).expect(200);
expect(res3.body.id).not.toBe(res1.body.id);
});
// ═══════════════ 场景 3: 事务回滚 ═══════════════
it('3. Snapshot/Outbox 创建失败 → 事务回滚 → 无残留', async () => {
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
const idemKey = `gt-rollback-${Date.now()}`;
// 使用不存在的 userId 触发 FK 失败 → 事务回滚
try {
await creationService.createJob({
userId: 'nonexistent-user-fk-fail',
jobType: 'synthetic_job',
triggerType: 'user_api',
targetType: 'synthetic',
targetId: 'rb-test',
idempotencyKey: idemKey,
});
} catch (_) { /* expected */ }
const jobs = await prisma.aiJob.findMany({ where: { idempotencyKey: idemKey } });
expect(jobs.length).toBe(0);
await prisma.$disconnect();
});
// ═══════════════ 场景 4: 幂等不重复创建 ═══════════════
it('4. 同 idempotencyKey 两次创建 → 只有 1 Job + 1 Snapshot + 1 Outbox', async () => {
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
const idemKey = `gt-idem-${Date.now()}`;
const j1 = await creationService.createJob({
userId: TEST_USER, jobType: 'synthetic_job', triggerType: 'user_api',
targetType: 'synthetic', targetId: 'idem-test', idempotencyKey: idemKey,
});
expect(j1).toBeDefined();
const jobs = await prisma.aiJob.findMany({ where: { idempotencyKey: idemKey } });
expect(jobs.length).toBe(1);
const snaps = await prisma.aiJobSnapshot.findMany({ where: { jobId: j1.id } });
expect(snaps.length).toBe(1);
const outboxes = await prisma.outboxEvent.findMany({ where: { aggregateId: j1.id } });
expect(outboxes.length).toBe(1);
await prisma.$disconnect();
});
// ═══════════════ 场景 5: 环境门控 ═══════════════
it('5. NODE_ENV=test + AI_JOB_SYNTHETIC_ENABLED=true → Synthetic 已注册', () => {
expect(process.env.NODE_ENV).toBe('test');
expect(process.env.AI_JOB_SYNTHETIC_ENABLED).toBe('true');
expect(registry.has('synthetic_job')).toBe(true);
});
// ═══════════════ JWT 保护 ═══════════════
it('GET /api/ai/jobs/:id → 401 without token', async () => {
await request(app.getHttpServer()).get('/api/ai/jobs/any').expect(401);
});
});