← Files Biological Sequence & Alignment ViewerARCHIVED FILE
src/msa/worker-client.test.ts
4.66 KB · Sep 30, 2026 · 23:01 UTC
import { afterEach, describe, expect, it, vi } from "vitest";
import { runMsaWorkerTask } from "./worker-client";
import type { MsaWorkerResponse } from "./worker-protocol";
afterEach(() => vi.useRealTimers());
describe("runMsaWorkerTask", () => {
it("falls back exactly once and terminates after a worker crash", async () => {
const worker = new FakeWorker((instance) => {
instance.onerror?.({
message: "worker crashed",
preventDefault: vi.fn(),
} as unknown as ErrorEvent);
});
const fallback = vi.fn(() => "main-thread-result");
await expect(
runMsaWorkerTask({
createWorker: () => worker,
fallback,
request: { contents: ">a\nAC", requestId: "parse-1", type: "parse" },
select: () => undefined,
}),
).resolves.toEqual({
fallbackReason: "worker crashed",
value: "main-thread-result",
});
expect(fallback).toHaveBeenCalledOnce();
expect(worker.terminate).toHaveBeenCalledOnce();
});
it("recovers from message decoding errors and postMessage exceptions", async () => {
const unreadable = new FakeWorker((instance) => {
instance.onmessageerror?.({ data: null } as MessageEvent<unknown>);
});
const first = await runMsaWorkerTask({
createWorker: () => unreadable,
fallback: () => 1,
request: { contents: "", requestId: "parse-2", type: "parse" },
select: () => undefined,
});
expect(first).toMatchObject({
fallbackReason: expect.stringContaining("unreadable"),
});
const throwing = new FakeWorker(() => {
throw new Error("clone failed");
});
const second = await runMsaWorkerTask({
createWorker: () => throwing,
fallback: () => 2,
request: { contents: "", requestId: "parse-3", type: "parse" },
select: () => undefined,
});
expect(second).toMatchObject({
fallbackReason: expect.stringContaining("clone failed"),
value: 2,
});
});
it("uses a watchdog when a worker never reaches a terminal response", async () => {
vi.useFakeTimers();
const worker = new FakeWorker(() => undefined);
const task = runMsaWorkerTask({
createWorker: () => worker,
fallback: () => "timed-out-fallback",
request: { contents: "", requestId: "parse-4", type: "parse" },
select: () => undefined,
timeoutMs: 50,
});
await vi.advanceTimersByTimeAsync(50);
await expect(task).resolves.toMatchObject({
fallbackReason: expect.stringContaining("50 ms"),
value: "timed-out-fallback",
});
});
it("accepts only the matching terminal response", async () => {
const response = {
analysis: { analysisId: "analysis-1" },
phase: "ready",
requestId: "analysis-1",
type: "analysis-result",
} as unknown as MsaWorkerResponse;
const worker = new FakeWorker((instance) => {
instance.onmessage?.({
data: response,
} as MessageEvent<MsaWorkerResponse>);
});
await expect(
runMsaWorkerTask({
createWorker: () => worker,
fallback: () => "fallback",
request: {
payload: {} as never,
requestId: "analysis-1",
type: "analyze",
},
select: (candidate) =>
candidate.type === "analysis-result"
? candidate.analysis.analysisId
: undefined,
}),
).resolves.toEqual({ value: "analysis-1" });
});
it("terminates a worker created after cancellation without running work", async () => {
const controller = new AbortController();
let resolveWorker!: (worker: FakeWorker) => void;
const workerPromise = new Promise<FakeWorker>((resolve) => {
resolveWorker = resolve;
});
const worker = new FakeWorker(() => undefined);
const fallback = vi.fn(() => "fallback");
const task = runMsaWorkerTask({
createWorker: () => workerPromise,
fallback,
request: { contents: "", requestId: "parse-5", type: "parse" },
select: () => undefined,
signal: controller.signal,
});
controller.abort();
resolveWorker(worker);
await expect(task).rejects.toMatchObject({ name: "AbortError" });
expect(worker.terminate).toHaveBeenCalledOnce();
expect(worker.postMessage).not.toHaveBeenCalled();
expect(fallback).not.toHaveBeenCalled();
});
});
class FakeWorker {
onerror: ((event: ErrorEvent) => void) | null = null;
onmessage: ((event: MessageEvent<MsaWorkerResponse>) => void) | null = null;
onmessageerror: ((event: MessageEvent<unknown>) => void) | null = null;
readonly terminate = vi.fn();
readonly postMessage = vi.fn(() => {
this.send(this);
});
constructor(private readonly send: (worker: FakeWorker) => void) {}
}
SHA-256: d5e70514998e0d2015bb0639c99c98477793ee2d6bf4883d9e9f946e1f74a4c4