Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions apps/dev/src/integration/translator/callEndpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import type { Payload, PayloadRequest } from "payload";
* handler code path. This is the faithful way to trigger a manual translation (`POST /translate/enqueue`)
* or read staleness (`GET /translate/stale/:collection_slug/:collection_id`) from a local-API test.
*
* @returns the endpoint's HTTP status + parsed JSON body.
* @returns the endpoint's HTTP status, and the parsed JSON body — `undefined` when the response
* has no body (204).
*/
export async function callEndpoint(
payload: Payload,
Expand All @@ -31,6 +32,6 @@ export async function callEndpoint(
} as unknown as PayloadRequest;

const res = await endpoint.handler(req);
const data = await res.json();
return { status: res.status, data };
const text = await res.text();
return { status: res.status, data: text ? JSON.parse(text) : undefined };
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,13 @@ describe("when one locale's provider fails", () => {
["fr", "failed"],
]);

// Payload backs a failed job off exponentially, so the picker would skip it for the next few
// seconds. Clearing the delay lets the retry happen now rather than making the spec wait.
await failing.payload.update({
collection: "payload-jobs" as "pages",
id: (docs[0] as { id: string | number }).id,
data: { waitUntil: null, processing: false } as never,
});

// Payload backs a failed job off exponentially; only the manual-run endpoint lifts that, so the
// retry has to go through it.
const before = failing.translateCount();
await failing.payload.jobs.run({ queue: "translations", limit: CRON_BATCH_LIMIT });
const retry = await callEndpoint(failing.payload, "post", "/translate/run/:id", {
routeParams: { id: String((docs[0] as { id: string | number }).id) },
});
expect(retry.status, "the retry endpoint did not accept the job").toBe(204);

expect(
failing.translateCount() - before,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -457,20 +457,20 @@ describe("PayloadJobsTaskRunner", () => {
});
});

it("clears what blocks the picker before retrying a failed job", async () => {
mockPayload.find.mockResolvedValue({
docs: [createJob({ error: { message: "provider down" } })],
});
it("clears the blockers even for a job that is neither running nor failed", async () => {
const stillDebounced = new Date(Date.now() + 60_000).toISOString();
const queued = createJob({ processing: false, waitUntil: stillDebounced });
mockPayload.find.mockResolvedValue({ docs: [queued] });

await runner.run("job-123");
const result = await runner.run("job-123");

expect(mockPayload.update).toHaveBeenCalledWith(
expect.objectContaining({
collection: "payload-jobs",
where: { id: { equals: "job-123" } },
data: { processing: false, hasError: false, error: null, waitUntil: null },
})
);
expect(result).toEqual({ success: true });
});

it("returns already_running when a job is genuinely in flight (fresh lock)", async () => {
Expand All @@ -484,6 +484,9 @@ describe("PayloadJobsTaskRunner", () => {

expect(result).toEqual({ success: false, error: "already_running" });
expect(mockPayload.jobs.run).not.toHaveBeenCalled();
// Clearing above this guard would reset `processing` on a live job and let a second worker
// take the same document.
expect(mockPayload.update).not.toHaveBeenCalled();
});

it("re-runs a job whose processing lock is stale (killed mid-run)", async () => {
Expand Down Expand Up @@ -549,7 +552,9 @@ describe("PayloadJobsTaskRunner", () => {
where: { id: { equals: "job-123" } },
limit: 1,
});
expect(mockPayload.update).not.toHaveBeenCalled();
expect(mockPayload.update.mock.invocationCallOrder[0]).toBeLessThan(
mockPayload.jobs.run.mock.invocationCallOrder[0]
);
expect(mockPayload.find).toHaveBeenCalledWith(
expect.objectContaining({
where: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,17 +187,16 @@ export class PayloadJobsTaskRunner implements TaskRunner {
if (job.processing && !this.isStale(job.updatedAt)) {
return { success: false, error: "already_running" };
}
if (job.processing || job.error) {
await this.clearPickerBlockers(taskId);
}
// Unconditional: a manual run also lifts the auto-translate debounce, not just the lock.
await this.clearPickerBlockers(taskId);

// Not `jobs.runByID`: payload 3.84.1 builds the picker guard (processing / hasError / waitUntil)
// only on the `where` path, so the id path re-runs a job that exhausted its retries.
const result = (await this.payload.jobs.run({
const result = await this.payload.jobs.run({
queue: this.config.queueName,
where: { id: { equals: taskId } },
limit: 1,
})) as { jobStatus?: Record<string, unknown> };
});

const pickerTookNothing = Object.keys(result?.jobStatus ?? {}).length === 0;
if (pickerTookNothing) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export type PayloadJob = {
updatedAt: string;
error?: unknown;
processing?: boolean | null;
waitUntil?: string | null;
input?: {
/** Document reference (flat text, ID-agnostic). Current shape. */
collection_slug?: string;
Expand Down
Loading