|
|
import { http, HttpResponse, delay } from 'msw' |
|
|
import type { JobStatus } from '../types' |
|
|
|
|
|
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:7860' |
|
|
|
|
|
|
|
|
interface MockJob { |
|
|
id: string |
|
|
caseId: string |
|
|
status: JobStatus |
|
|
progress: number |
|
|
progressMessage: string |
|
|
elapsedSeconds: number |
|
|
fastMode: boolean |
|
|
createdAt: number |
|
|
} |
|
|
|
|
|
const mockJobs = new Map<string, MockJob>() |
|
|
let jobCounter = 0 |
|
|
|
|
|
|
|
|
|
|
|
let mockJobDurationMs = 500 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function setMockJobDuration(durationMs: number): void { |
|
|
mockJobDurationMs = durationMs |
|
|
} |
|
|
|
|
|
|
|
|
function getJobProgress(job: MockJob): MockJob { |
|
|
const elapsed = (Date.now() - job.createdAt) / 1000 |
|
|
const duration = mockJobDurationMs / 1000 |
|
|
|
|
|
if (job.status === 'completed' || job.status === 'failed') { |
|
|
return job |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const progress20 = duration * 0.2 |
|
|
const progress60 = duration * 0.6 |
|
|
const progress90 = duration * 0.9 |
|
|
|
|
|
if (elapsed < progress20) { |
|
|
return { ...job, status: 'running', progress: 10, progressMessage: 'Loading case data...', elapsedSeconds: elapsed } |
|
|
} else if (elapsed < progress60) { |
|
|
return { ...job, status: 'running', progress: 30, progressMessage: 'Running DeepISLES inference...', elapsedSeconds: elapsed } |
|
|
} else if (elapsed < progress90) { |
|
|
return { ...job, status: 'running', progress: 70, progressMessage: 'Processing results...', elapsedSeconds: elapsed } |
|
|
} else if (elapsed < duration) { |
|
|
return { ...job, status: 'running', progress: 90, progressMessage: 'Computing metrics...', elapsedSeconds: elapsed } |
|
|
} else { |
|
|
|
|
|
return { ...job, status: 'completed', progress: 100, progressMessage: 'Segmentation complete', elapsedSeconds: elapsed } |
|
|
} |
|
|
} |
|
|
|
|
|
export const handlers = [ |
|
|
|
|
|
http.get(`${API_BASE}/api/cases`, async () => { |
|
|
await delay(100) |
|
|
return HttpResponse.json({ |
|
|
cases: ['sub-stroke0001', 'sub-stroke0002', 'sub-stroke0003'], |
|
|
}) |
|
|
}), |
|
|
|
|
|
|
|
|
http.post(`${API_BASE}/api/segment`, async ({ request }) => { |
|
|
const body = (await request.json()) as { case_id: string; fast_mode?: boolean } |
|
|
await delay(50) |
|
|
|
|
|
|
|
|
const jobId = `mock-${++jobCounter}` |
|
|
const job: MockJob = { |
|
|
id: jobId, |
|
|
caseId: body.case_id, |
|
|
status: 'pending', |
|
|
progress: 0, |
|
|
progressMessage: 'Job queued', |
|
|
elapsedSeconds: 0, |
|
|
fastMode: body.fast_mode !== false, |
|
|
createdAt: Date.now(), |
|
|
} |
|
|
mockJobs.set(jobId, job) |
|
|
|
|
|
|
|
|
return HttpResponse.json( |
|
|
{ |
|
|
jobId: jobId, |
|
|
status: 'pending', |
|
|
message: `Segmentation job queued for ${body.case_id}`, |
|
|
}, |
|
|
{ status: 202 } |
|
|
) |
|
|
}), |
|
|
|
|
|
|
|
|
http.get(`${API_BASE}/api/jobs/:jobId`, async ({ params }) => { |
|
|
const jobId = params.jobId as string |
|
|
await delay(50) |
|
|
|
|
|
const job = mockJobs.get(jobId) |
|
|
if (!job) { |
|
|
return HttpResponse.json( |
|
|
{ detail: `Job not found: ${jobId}. Jobs expire after 1 hour.` }, |
|
|
{ status: 404 } |
|
|
) |
|
|
} |
|
|
|
|
|
|
|
|
const updatedJob = getJobProgress(job) |
|
|
mockJobs.set(jobId, updatedJob) |
|
|
|
|
|
|
|
|
const response: Record<string, unknown> = { |
|
|
jobId: updatedJob.id, |
|
|
status: updatedJob.status, |
|
|
progress: updatedJob.progress, |
|
|
progressMessage: updatedJob.progressMessage, |
|
|
elapsedSeconds: Math.round(updatedJob.elapsedSeconds * 100) / 100, |
|
|
} |
|
|
|
|
|
|
|
|
if (updatedJob.status === 'completed') { |
|
|
response.result = { |
|
|
caseId: updatedJob.caseId, |
|
|
diceScore: 0.847, |
|
|
volumeMl: 15.32, |
|
|
elapsedSeconds: updatedJob.fastMode ? 12.5 : 45.0, |
|
|
dwiUrl: `${API_BASE}/files/${jobId}/${updatedJob.caseId}/dwi.nii.gz`, |
|
|
predictionUrl: `${API_BASE}/files/${jobId}/${updatedJob.caseId}/prediction.nii.gz`, |
|
|
} |
|
|
} |
|
|
|
|
|
return HttpResponse.json(response) |
|
|
}), |
|
|
] |
|
|
|
|
|
|
|
|
export const errorHandlers = { |
|
|
casesServerError: http.get(`${API_BASE}/api/cases`, () => { |
|
|
return HttpResponse.json( |
|
|
{ detail: 'Internal server error' }, |
|
|
{ status: 500 } |
|
|
) |
|
|
}), |
|
|
|
|
|
casesNetworkError: http.get(`${API_BASE}/api/cases`, () => { |
|
|
return HttpResponse.error() |
|
|
}), |
|
|
|
|
|
segmentCreateError: http.post(`${API_BASE}/api/segment`, () => { |
|
|
return HttpResponse.json( |
|
|
{ detail: 'Failed to create job: case not found' }, |
|
|
{ status: 400 } |
|
|
) |
|
|
}), |
|
|
|
|
|
jobNotFound: http.get(`${API_BASE}/api/jobs/:jobId`, () => { |
|
|
return HttpResponse.json( |
|
|
{ detail: 'Job not found or expired' }, |
|
|
{ status: 404 } |
|
|
) |
|
|
}), |
|
|
|
|
|
|
|
|
jobFailed: [ |
|
|
http.post(`${API_BASE}/api/segment`, async ({ request }) => { |
|
|
const body = (await request.json()) as { case_id: string } |
|
|
const jobId = `fail-${++jobCounter}` |
|
|
mockJobs.set(jobId, { |
|
|
id: jobId, |
|
|
caseId: body.case_id, |
|
|
status: 'failed', |
|
|
progress: 30, |
|
|
progressMessage: 'Error occurred', |
|
|
elapsedSeconds: 5.2, |
|
|
fastMode: true, |
|
|
createdAt: Date.now(), |
|
|
}) |
|
|
return HttpResponse.json( |
|
|
{ jobId, status: 'pending', message: 'Job queued' }, |
|
|
{ status: 202 } |
|
|
) |
|
|
}), |
|
|
http.get(`${API_BASE}/api/jobs/:jobId`, ({ params }) => { |
|
|
const jobId = params.jobId as string |
|
|
const job = mockJobs.get(jobId) |
|
|
if (!job) { |
|
|
return HttpResponse.json({ detail: 'Not found' }, { status: 404 }) |
|
|
} |
|
|
return HttpResponse.json({ |
|
|
jobId: job.id, |
|
|
status: 'failed', |
|
|
progress: 30, |
|
|
progressMessage: 'Error occurred', |
|
|
elapsedSeconds: 5.2, |
|
|
error: 'Segmentation failed: out of memory', |
|
|
}) |
|
|
}), |
|
|
], |
|
|
} |
|
|
|