All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 49s
- 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>
112 lines
2.9 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
}
|
|
}
|