|
1 | 1 | import { sidequestTest, SidequestTestFixture } from "@/tests/fixture"; |
2 | | -import { FailedResult, JobData } from "@sidequest/core"; |
| 2 | +import { FailedResult, JobCanceled, JobData, JobTimeout } from "@sidequest/core"; |
3 | 3 | import { existsSync, unlinkSync } from "node:fs"; |
4 | 4 | import { unlink, writeFile } from "node:fs/promises"; |
5 | 5 | import { join, resolve } from "node:path"; |
| 6 | +import { MessageChannel } from "node:worker_threads"; |
6 | 7 | import { vi } from "vitest"; |
| 8 | +import { AbortAwareJob } from "../test-jobs/abort-aware-job"; |
7 | 9 | import { DummyJob } from "../test-jobs/dummy-job"; |
8 | 10 | import { DummyJobWithArgs } from "../test-jobs/dummy-job-with-args"; |
9 | 11 | import { importSidequest } from "../utils/import"; |
@@ -80,6 +82,80 @@ describe("runner.ts", () => { |
80 | 82 | injectSpy.mockRestore(); |
81 | 83 | }); |
82 | 84 |
|
| 85 | + sidequestTest("a job receives the abort signal and its reason and stops", async ({ backend, config }) => { |
| 86 | + const job = new AbortAwareJob(); |
| 87 | + await job.ready(); |
| 88 | + const abortJobData = await backend.createNewJob({ |
| 89 | + class: job.className, |
| 90 | + script: job.script, |
| 91 | + args: [], |
| 92 | + attempt: 0, |
| 93 | + queue: "default", |
| 94 | + constructor_args: [], |
| 95 | + state: "waiting", |
| 96 | + }); |
| 97 | + |
| 98 | + const controller = new AbortController(); |
| 99 | + const resultPromise = run({ jobData: abortJobData, config, inline: true, signal: controller.signal }); |
| 100 | + controller.abort(new JobCanceled()); |
| 101 | + |
| 102 | + // The job resolves once it observes the abort (whether before it starts or while awaiting it). |
| 103 | + const result = (await resultPromise) as FailedResult; |
| 104 | + expect(result.type).toBe("failed"); |
| 105 | + expect(result.error.message).toContain("JobCanceled"); |
| 106 | + }); |
| 107 | + |
| 108 | + sidequestTest("a job sees an already-aborted signal before it starts", async ({ backend, config }) => { |
| 109 | + const job = new AbortAwareJob(); |
| 110 | + await job.ready(); |
| 111 | + const abortJobData = await backend.createNewJob({ |
| 112 | + class: job.className, |
| 113 | + script: job.script, |
| 114 | + args: [], |
| 115 | + attempt: 0, |
| 116 | + queue: "default", |
| 117 | + constructor_args: [], |
| 118 | + state: "waiting", |
| 119 | + }); |
| 120 | + |
| 121 | + const controller = new AbortController(); |
| 122 | + controller.abort(new JobTimeout(50)); |
| 123 | + |
| 124 | + const result = (await run({ |
| 125 | + jobData: abortJobData, |
| 126 | + config, |
| 127 | + inline: true, |
| 128 | + signal: controller.signal, |
| 129 | + })) as FailedResult; |
| 130 | + expect(result.type).toBe("failed"); |
| 131 | + expect(result.error.message).toContain("aborted before start: JobTimeout"); |
| 132 | + }); |
| 133 | + |
| 134 | + sidequestTest("a thread job is aborted via the abort port (rebuilds the reason)", async ({ backend, config }) => { |
| 135 | + const job = new AbortAwareJob(); |
| 136 | + await job.ready(); |
| 137 | + const abortJobData = await backend.createNewJob({ |
| 138 | + class: job.className, |
| 139 | + script: job.script, |
| 140 | + args: [], |
| 141 | + attempt: 0, |
| 142 | + queue: "default", |
| 143 | + constructor_args: [], |
| 144 | + state: "waiting", |
| 145 | + }); |
| 146 | + |
| 147 | + const channel = new MessageChannel(); |
| 148 | + // Thread path: no live signal, the abort arrives over the transferred port. |
| 149 | + const resultPromise = run({ jobData: abortJobData, config, abortPort: channel.port2 }); |
| 150 | + channel.port1.postMessage({ kind: "timeout", timeoutMs: 1234 }); |
| 151 | + |
| 152 | + const result = (await resultPromise) as FailedResult; |
| 153 | + expect(result.type).toBe("failed"); |
| 154 | + // The worker reconstructs the JobTimeout reason from the port message. |
| 155 | + expect(result.error.message).toContain("JobTimeout"); |
| 156 | + channel.port1.close(); |
| 157 | + }); |
| 158 | + |
83 | 159 | sidequestTest("fails with invalid script", async ({ config }) => { |
84 | 160 | jobData.script = "invalid!"; |
85 | 161 | const result = (await run({ jobData, config })) as FailedResult; |
|
0 commit comments