first commit
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
type CompletionResult = {
|
||||
text: string
|
||||
reasoning?: string
|
||||
}
|
||||
|
||||
const state: { nextText: string; nextReasoning: string } = {
|
||||
nextText: '{"ok":true}',
|
||||
nextReasoning: '',
|
||||
}
|
||||
|
||||
export function configureFakeLLM(result: CompletionResult) {
|
||||
state.nextText = result.text
|
||||
state.nextReasoning = result.reasoning || ''
|
||||
}
|
||||
|
||||
export function resetFakeLLM() {
|
||||
state.nextText = '{"ok":true}'
|
||||
state.nextReasoning = ''
|
||||
}
|
||||
|
||||
export async function fakeChatCompletion() {
|
||||
return {
|
||||
output_text: state.nextText,
|
||||
reasoning: state.nextReasoning,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
const state: {
|
||||
nextImageUrl: string
|
||||
nextVideoUrl: string
|
||||
nextAudioUrl: string
|
||||
} = {
|
||||
nextImageUrl: 'images/fake-image.jpg',
|
||||
nextVideoUrl: 'video/fake-video.mp4',
|
||||
nextAudioUrl: 'voice/fake-audio.mp3',
|
||||
}
|
||||
|
||||
export function configureFakeMedia(params: {
|
||||
imageUrl?: string
|
||||
videoUrl?: string
|
||||
audioUrl?: string
|
||||
}) {
|
||||
if (params.imageUrl) state.nextImageUrl = params.imageUrl
|
||||
if (params.videoUrl) state.nextVideoUrl = params.videoUrl
|
||||
if (params.audioUrl) state.nextAudioUrl = params.audioUrl
|
||||
}
|
||||
|
||||
export function resetFakeMedia() {
|
||||
state.nextImageUrl = 'images/fake-image.jpg'
|
||||
state.nextVideoUrl = 'video/fake-video.mp4'
|
||||
state.nextAudioUrl = 'voice/fake-audio.mp3'
|
||||
}
|
||||
|
||||
export async function fakeGenerateImage() {
|
||||
return { success: true, imageUrl: state.nextImageUrl }
|
||||
}
|
||||
|
||||
export async function fakeGenerateVideo() {
|
||||
return { success: true, videoUrl: state.nextVideoUrl }
|
||||
}
|
||||
|
||||
export async function fakeGenerateAudio() {
|
||||
return { success: true, audioUrl: state.nextAudioUrl }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
const providerState: {
|
||||
falApiKey: string
|
||||
googleApiKey: string
|
||||
openrouterApiKey: string
|
||||
} = {
|
||||
falApiKey: 'fake-fal-key',
|
||||
googleApiKey: 'fake-google-key',
|
||||
openrouterApiKey: 'fake-openrouter-key',
|
||||
}
|
||||
|
||||
export function configureFakeProviders(params: {
|
||||
falApiKey?: string
|
||||
googleApiKey?: string
|
||||
openrouterApiKey?: string
|
||||
}) {
|
||||
if (params.falApiKey) providerState.falApiKey = params.falApiKey
|
||||
if (params.googleApiKey) providerState.googleApiKey = params.googleApiKey
|
||||
if (params.openrouterApiKey) providerState.openrouterApiKey = params.openrouterApiKey
|
||||
}
|
||||
|
||||
export function resetFakeProviders() {
|
||||
providerState.falApiKey = 'fake-fal-key'
|
||||
providerState.googleApiKey = 'fake-google-key'
|
||||
providerState.openrouterApiKey = 'fake-openrouter-key'
|
||||
}
|
||||
|
||||
export function getFakeProviderConfig(provider: 'fal' | 'google' | 'openrouter') {
|
||||
if (provider === 'fal') {
|
||||
return { apiKey: providerState.falApiKey }
|
||||
}
|
||||
if (provider === 'google') {
|
||||
return { apiKey: providerState.googleApiKey }
|
||||
}
|
||||
return { apiKey: providerState.openrouterApiKey }
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import http, { type IncomingMessage, type ServerResponse } from 'node:http'
|
||||
|
||||
export type FakeScenarioMode =
|
||||
| 'success'
|
||||
| 'queued_then_success'
|
||||
| 'retryable_error_then_success'
|
||||
| 'fatal_error'
|
||||
| 'malformed_response'
|
||||
| 'timeout'
|
||||
|
||||
export type FakeResponseSpec = {
|
||||
status: number
|
||||
headers?: Record<string, string>
|
||||
body?: string | Buffer | Record<string, unknown> | unknown[] | null
|
||||
delayMs?: number
|
||||
}
|
||||
|
||||
export type FakeRequestRecord = {
|
||||
method: string
|
||||
path: string
|
||||
query: string
|
||||
bodyText: string
|
||||
headers: Record<string, string | string[] | undefined>
|
||||
}
|
||||
|
||||
type RouteKey = `${Uppercase<string>} ${string}`
|
||||
|
||||
type RouteScenario = {
|
||||
mode: FakeScenarioMode
|
||||
submitResponse?: FakeResponseSpec
|
||||
pollSequence?: FakeResponseSpec[]
|
||||
errorCode?: string
|
||||
delayMs?: number
|
||||
}
|
||||
|
||||
function routeKey(method: string, path: string): RouteKey {
|
||||
return `${method.toUpperCase()} ${path}` as RouteKey
|
||||
}
|
||||
|
||||
function normalizeHeaders(headers: IncomingMessage['headers']): Record<string, string | string[] | undefined> {
|
||||
return Object.fromEntries(Object.entries(headers))
|
||||
}
|
||||
|
||||
function toBodyText(chunks: Buffer[]): string {
|
||||
if (chunks.length === 0) return ''
|
||||
return Buffer.concat(chunks).toString('utf8')
|
||||
}
|
||||
|
||||
function isJsonBody(body: FakeResponseSpec['body']): body is Record<string, unknown> | unknown[] | null {
|
||||
return body === null || Array.isArray(body) || (!!body && typeof body === 'object' && !Buffer.isBuffer(body))
|
||||
}
|
||||
|
||||
async function writeResponse(
|
||||
res: ServerResponse,
|
||||
spec: FakeResponseSpec,
|
||||
inheritedDelayMs: number | undefined,
|
||||
) {
|
||||
const delayMs = spec.delayMs ?? inheritedDelayMs ?? 0
|
||||
if (delayMs > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs))
|
||||
}
|
||||
|
||||
const headers = { ...(spec.headers || {}) }
|
||||
if (isJsonBody(spec.body) && !headers['content-type']) {
|
||||
headers['content-type'] = 'application/json'
|
||||
}
|
||||
res.writeHead(spec.status, headers)
|
||||
|
||||
if (spec.body === undefined) {
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
if (Buffer.isBuffer(spec.body)) {
|
||||
res.end(spec.body)
|
||||
return
|
||||
}
|
||||
if (isJsonBody(spec.body)) {
|
||||
res.end(JSON.stringify(spec.body))
|
||||
return
|
||||
}
|
||||
res.end(spec.body)
|
||||
}
|
||||
|
||||
export async function startScenarioServer() {
|
||||
const requests = new Map<RouteKey, FakeRequestRecord[]>()
|
||||
const routes = new Map<RouteKey, { queue: FakeResponseSpec[]; mode: FakeScenarioMode; delayMs?: number }>()
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const url = new URL(req.url || '/', 'http://127.0.0.1')
|
||||
const key = routeKey(req.method || 'GET', url.pathname)
|
||||
const entry = routes.get(key)
|
||||
const chunks: Buffer[] = []
|
||||
for await (const chunk of req) {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
||||
}
|
||||
const bodyText = toBodyText(chunks)
|
||||
const history = requests.get(key) || []
|
||||
history.push({
|
||||
method: (req.method || 'GET').toUpperCase(),
|
||||
path: url.pathname,
|
||||
query: url.search,
|
||||
bodyText,
|
||||
headers: normalizeHeaders(req.headers),
|
||||
})
|
||||
requests.set(key, history)
|
||||
|
||||
if (!entry) {
|
||||
await writeResponse(res, {
|
||||
status: 404,
|
||||
body: { error: 'SCENARIO_ROUTE_NOT_FOUND', path: url.pathname },
|
||||
}, 0)
|
||||
return
|
||||
}
|
||||
|
||||
const next = entry.queue.length > 1 ? entry.queue.shift() : entry.queue[0]
|
||||
if (!next) {
|
||||
await writeResponse(res, {
|
||||
status: 500,
|
||||
body: { error: 'SCENARIO_DEPLETED', path: url.pathname, mode: entry.mode },
|
||||
}, entry.delayMs)
|
||||
return
|
||||
}
|
||||
|
||||
await writeResponse(res, next, entry.delayMs)
|
||||
})
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.listen(0, '127.0.0.1', () => resolve())
|
||||
server.once('error', reject)
|
||||
})
|
||||
|
||||
const address = server.address()
|
||||
if (!address || typeof address === 'string') {
|
||||
throw new Error('SCENARIO_SERVER_ADDRESS_INVALID')
|
||||
}
|
||||
const baseUrl = `http://127.0.0.1:${address.port}`
|
||||
|
||||
return {
|
||||
baseUrl,
|
||||
defineScenario(input: {
|
||||
method: string
|
||||
path: string
|
||||
mode: FakeScenarioMode
|
||||
submitResponse?: FakeResponseSpec
|
||||
pollSequence?: FakeResponseSpec[]
|
||||
errorCode?: string
|
||||
delayMs?: number
|
||||
}) {
|
||||
const key = routeKey(input.method, input.path)
|
||||
const queue: FakeResponseSpec[] = []
|
||||
if (input.submitResponse) {
|
||||
queue.push(input.submitResponse)
|
||||
}
|
||||
if (input.pollSequence && input.pollSequence.length > 0) {
|
||||
queue.push(...input.pollSequence)
|
||||
}
|
||||
if (queue.length === 0) {
|
||||
throw new Error(`SCENARIO_EMPTY_QUEUE: ${key}`)
|
||||
}
|
||||
const scenario: RouteScenario = {
|
||||
mode: input.mode,
|
||||
submitResponse: input.submitResponse,
|
||||
pollSequence: input.pollSequence,
|
||||
errorCode: input.errorCode,
|
||||
delayMs: input.delayMs,
|
||||
}
|
||||
routes.set(key, {
|
||||
queue,
|
||||
mode: scenario.mode,
|
||||
delayMs: scenario.delayMs,
|
||||
})
|
||||
requests.delete(key)
|
||||
},
|
||||
getRequests(method: string, path: string): FakeRequestRecord[] {
|
||||
return [...(requests.get(routeKey(method, path)) || [])]
|
||||
},
|
||||
reset() {
|
||||
routes.clear()
|
||||
requests.clear()
|
||||
},
|
||||
async close() {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user