api-server/test/helpers/mock-ai-provider.ts
wangdl ffea415613
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 49s
feat(M-AI-01-09): replace mock E2E with real API/Worker integration test
- NEW test/helpers/mock-ai-provider.ts: Mock AI HTTP server with
  normal/timeout/429/500/invalid-json behavior controls
- NEW test/helpers/integration-harness.ts: spawn real API/Worker
  processes, HTTP helpers, PrismaClient-based DB helpers
- NEW test/m-ai-01-09.worker-int-spec.ts: real integration test
  using actual MySQL/Redis/BullMQ/HTTP (Active Recall, Feynman,
  Document Import, SIGKILL recovery, provider faults, idempotency)
- NEW test/jest-worker-integration.json: Jest config with NO mocks
- DELETE test/m-ai-01-09.e2e-spec.ts: removed mock-based pseudo-e2e
- UPDATE test/run-integration-ci.sh: uses real integration config
- UPDATE package.json: test:integration:worker uses real config

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-19 22:06:52 +08:00

112 lines
2.9 KiB
TypeScript

/**
* Mock AI Provider HTTP Server
* 用于集成测试中替代 DeepSeek API。
* 支持: 正常响应 / 超时 / 429 / 500 / 非法 JSON
*/
import { createServer, Server, IncomingMessage, ServerResponse } from 'http';
interface Behavior {
status: number;
response: unknown;
delayMs?: number;
}
const DEFAULT_RESPONSE = {
id: 'mock-deepseek-001',
object: 'chat.completion',
created: Math.floor(Date.now() / 1000),
model: 'deepseek-chat',
choices: [{
index: 0,
message: {
role: 'assistant',
content: JSON.stringify({
score: 85,
summary: 'Mock AI: good understanding of the topic',
strengths: ['Clear explanation', 'Accurate facts'],
weaknesses: ['Could elaborate more on edge cases'],
confidence: 0.9,
learningState: 'proficient',
riskLevel: 'low',
}),
},
finish_reason: 'stop',
}],
usage: { prompt_tokens: 100, completion_tokens: 50, total_tokens: 150 },
};
export class MockAIProvider {
private server: Server | null = null;
private port = 0;
private behavior: Behavior = { status: 200, response: DEFAULT_RESPONSE };
// ========== behavior controls ==========
setNormal() {
this.behavior = { status: 200, response: DEFAULT_RESPONSE };
}
setTimeout() {
this.behavior = { status: 200, response: DEFAULT_RESPONSE, delayMs: 120_000 };
}
set429() {
this.behavior = { status: 429, response: { error: { message: 'Rate limit exceeded', type: 'rate_limit_error' } } };
}
set500() {
this.behavior = { status: 500, response: { error: { message: 'Internal server error' } } };
}
setInvalidJson() {
this.behavior = { status: 200, response: 'not valid json {{{{{' };
}
get url(): string {
return `http://127.0.0.1:${this.port}`;
}
async start(): Promise<number> {
return new Promise((resolve, reject) => {
this.server = createServer((_req: IncomingMessage, res: ServerResponse) => {
const b = this.behavior;
if (b.delayMs) {
// Simulate timeout — never respond
return;
}
if (typeof b.response === 'string') {
res.writeHead(b.status, { 'Content-Type': 'text/plain' });
res.end(b.response);
} else {
res.writeHead(b.status, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(b.response));
}
});
this.server.on('error', reject);
this.server.listen(0, '127.0.0.1', () => {
const addr = this.server!.address();
if (addr && typeof addr === 'object') {
this.port = addr.port;
resolve(this.port);
} else {
reject(new Error('Could not get port'));
}
});
});
}
async stop(): Promise<void> {
return new Promise((resolve) => {
if (this.server) {
this.server.close(() => resolve());
this.server = null;
} else {
resolve();
}
});
}
}