Shaheer Sarfaraz f3c164d252
feat(pipeline): parallelize discovery/process via evolved asyncPool (#211)
* feat(pipeline): centralize concurrency hooks and parallelize discovery/process steps

* feat(orchestrator): unify single and bulk job actions API

* job actions de-bulk-ified

* application inbox section debulk

* chore(orchestrator): remove remaining bulk wording from job action flow

* select multiple to skip with shortcut

* comments

* coomeents

* fix progress ordinal and add jobs actions payload examples
2026-02-20 16:49:13 +00:00

1047 lines
36 KiB
TypeScript

import type { Server } from "node:http";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { startServer, stopServer } from "./test-utils";
describe.sequential("Jobs API routes", () => {
let server: Server;
let baseUrl: string;
let closeDb: () => void;
let tempDir: string;
beforeEach(async () => {
({ server, baseUrl, closeDb, tempDir } = await startServer());
});
afterEach(async () => {
await stopServer({ server, closeDb, tempDir });
});
it("lists jobs and supports status filtering", async () => {
const { createJob } = await import("../../repositories/jobs");
const job = await createJob({
source: "manual",
title: "Test Role",
employer: "Acme",
jobUrl: "https://example.com/job/1",
jobDescription: "Test description",
});
const listRes = await fetch(`${baseUrl}/api/jobs`);
const listBody = await listRes.json();
expect(listBody.ok).toBe(true);
expect(listBody.data.total).toBe(1);
expect(listBody.data.jobs[0].id).toBe(job.id);
expect(typeof listBody.data.revision).toBe("string");
const filteredRes = await fetch(`${baseUrl}/api/jobs?status=skipped`);
const filteredBody = await filteredRes.json();
expect(filteredBody.data.total).toBe(0);
expect(typeof filteredBody.data.revision).toBe("string");
});
it("supports lightweight and full jobs list views", async () => {
const { createJob } = await import("../../repositories/jobs");
await createJob({
source: "manual",
title: "List View Role",
employer: "Acme",
jobUrl: "https://example.com/job/list-view",
jobDescription: "Heavy description that should not be in list mode",
});
const listRes = await fetch(`${baseUrl}/api/jobs?view=list`);
const listBody = await listRes.json();
expect(listRes.status).toBe(200);
expect(listBody.ok).toBe(true);
expect(typeof listBody.meta.requestId).toBe("string");
expect(listBody.data.jobs[0].id).toBeTruthy();
expect(listBody.data.jobs[0].title).toBe("List View Role");
expect(listBody.data.jobs[0]).not.toHaveProperty("jobDescription");
expect(typeof listBody.data.revision).toBe("string");
const fullRes = await fetch(`${baseUrl}/api/jobs?view=full`);
const fullBody = await fullRes.json();
expect(fullRes.status).toBe(200);
expect(fullBody.ok).toBe(true);
expect(fullBody.data.jobs[0].title).toBe("List View Role");
expect(fullBody.data.jobs[0]).toHaveProperty("jobDescription");
expect(typeof fullBody.data.revision).toBe("string");
const defaultRes = await fetch(`${baseUrl}/api/jobs`);
const defaultBody = await defaultRes.json();
expect(defaultRes.status).toBe(200);
expect(defaultBody.ok).toBe(true);
expect(defaultBody.data.jobs[0]).not.toHaveProperty("jobDescription");
expect(typeof defaultBody.data.revision).toBe("string");
});
it("returns jobs revision and supports status filtering", async () => {
const { createJob, updateJob } = await import("../../repositories/jobs");
const readyJob = await createJob({
source: "manual",
title: "Ready Role",
employer: "Acme",
jobUrl: "https://example.com/job/revision-ready",
jobDescription: "Ready description",
});
const appliedJob = await createJob({
source: "manual",
title: "Applied Role",
employer: "Beta",
jobUrl: "https://example.com/job/revision-applied",
jobDescription: "Applied description",
});
await updateJob(readyJob.id, { status: "ready" });
await updateJob(appliedJob.id, { status: "applied" });
const allRes = await fetch(`${baseUrl}/api/jobs/revision`);
const allBody = await allRes.json();
expect(allRes.status).toBe(200);
expect(allBody.ok).toBe(true);
expect(typeof allBody.meta.requestId).toBe("string");
expect(typeof allBody.data.revision).toBe("string");
expect(allBody.data.total).toBe(2);
expect(allBody.data.latestUpdatedAt).toBeTruthy();
expect(allBody.data.statusFilter).toBeNull();
const filteredRes = await fetch(
`${baseUrl}/api/jobs/revision?status=applied,ready`,
);
const filteredBody = await filteredRes.json();
expect(filteredRes.status).toBe(200);
expect(filteredBody.ok).toBe(true);
expect(filteredBody.data.total).toBe(2);
expect(filteredBody.data.statusFilter).toBe("applied,ready");
expect(typeof filteredBody.data.revision).toBe("string");
});
it("rejects invalid jobs list view query", async () => {
const res = await fetch(`${baseUrl}/api/jobs?view=compact`);
const body = await res.json();
expect(res.status).toBe(400);
expect(body.ok).toBe(false);
expect(body.error.code).toBe("INVALID_REQUEST");
expect(typeof body.meta.requestId).toBe("string");
});
it("returns 404 for missing jobs", async () => {
const res = await fetch(`${baseUrl}/api/jobs/missing-id`);
expect(res.status).toBe(404);
});
it("updates core job detail fields", async () => {
const { createJob } = await import("../../repositories/jobs");
const job = await createJob({
source: "manual",
title: "Original Title",
employer: "Original Employer",
jobUrl: "https://example.com/job/core-fields",
jobDescription: "Original description",
});
const res = await fetch(`${baseUrl}/api/jobs/${job.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: "Updated Title",
employer: "Updated Employer",
jobUrl: "https://example.com/job/core-fields-updated",
applicationLink: "https://example.com/apply/core-fields-updated",
location: "London, UK",
salary: "GBP 100k",
deadline: "2026-03-31",
jobDescription: "Updated description",
}),
});
const body = await res.json();
expect(res.status).toBe(200);
expect(body.ok).toBe(true);
expect(body.data.title).toBe("Updated Title");
expect(body.data.employer).toBe("Updated Employer");
expect(body.data.jobUrl).toBe(
"https://example.com/job/core-fields-updated",
);
expect(body.data.applicationLink).toBe(
"https://example.com/apply/core-fields-updated",
);
expect(body.data.location).toBe("London, UK");
expect(body.data.salary).toBe("GBP 100k");
expect(body.data.deadline).toBe("2026-03-31");
expect(body.data.jobDescription).toBe("Updated description");
expect(typeof body.meta.requestId).toBe("string");
});
it("blocks enabling tracer links when readiness check fails", async () => {
const { createJob } = await import("../../repositories/jobs");
const job = await createJob({
source: "manual",
title: "Tracer Blocked",
employer: "Example Co",
jobUrl: "https://example.com/job/tracer-blocked",
jobDescription: "Test description",
});
const previousBaseUrl = process.env.JOBOPS_PUBLIC_BASE_URL;
process.env.JOBOPS_PUBLIC_BASE_URL = "https://my-jobops.example.com";
const realFetch = global.fetch;
const mockFetch = vi.fn(async (input: any, init?: RequestInit) => {
const url = typeof input === "string" ? input : input.toString();
if (url === "https://my-jobops.example.com/health") {
return new Response("unavailable", { status: 503 });
}
return realFetch(input, init);
});
vi.stubGlobal("fetch", mockFetch);
try {
const res = await fetch(`${baseUrl}/api/jobs/${job.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ tracerLinksEnabled: true }),
});
const body = await res.json();
expect(res.status).toBe(409);
expect(body.ok).toBe(false);
expect(body.error.code).toBe("CONFLICT");
expect(body.error.message).toMatch(/health check returned http 503/i);
expect(typeof body.meta.requestId).toBe("string");
} finally {
vi.unstubAllGlobals();
if (previousBaseUrl === undefined) {
delete process.env.JOBOPS_PUBLIC_BASE_URL;
} else {
process.env.JOBOPS_PUBLIC_BASE_URL = previousBaseUrl;
}
}
});
it("allows updates for already-enabled tracer links without re-gating", async () => {
const { createJob } = await import("../../repositories/jobs");
const { updateJob } = await import("../../repositories/jobs");
const job = await createJob({
source: "manual",
title: "Tracer Already On",
employer: "Example Co",
jobUrl: "https://example.com/job/tracer-enabled",
jobDescription: "Test description",
});
await updateJob(job.id, { tracerLinksEnabled: true });
const previousBaseUrl = process.env.JOBOPS_PUBLIC_BASE_URL;
process.env.JOBOPS_PUBLIC_BASE_URL = "https://my-jobops.example.com";
const realFetch = global.fetch;
const mockFetch = vi.fn(async (input: any, init?: RequestInit) => {
const url = typeof input === "string" ? input : input.toString();
if (url === "https://my-jobops.example.com/health") {
return new Response("unavailable", { status: 503 });
}
return realFetch(input, init);
});
vi.stubGlobal("fetch", mockFetch);
try {
const res = await fetch(`${baseUrl}/api/jobs/${job.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: "Tracer Already On (Edited)",
tracerLinksEnabled: true,
}),
});
const body = await res.json();
expect(res.status).toBe(200);
expect(body.ok).toBe(true);
expect(body.data.title).toBe("Tracer Already On (Edited)");
expect(body.data.tracerLinksEnabled).toBe(true);
expect(mockFetch).not.toHaveBeenCalledWith(
"https://my-jobops.example.com/health",
expect.anything(),
);
} finally {
vi.unstubAllGlobals();
if (previousBaseUrl === undefined) {
delete process.env.JOBOPS_PUBLIC_BASE_URL;
} else {
process.env.JOBOPS_PUBLIC_BASE_URL = previousBaseUrl;
}
}
});
it("returns 404 when patching a missing job", async () => {
const res = await fetch(`${baseUrl}/api/jobs/missing-id`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "Updated Title" }),
});
const body = await res.json();
expect(res.status).toBe(404);
expect(body.ok).toBe(false);
expect(body.error.code).toBe("NOT_FOUND");
expect(typeof body.meta.requestId).toBe("string");
});
it("prefers JOBOPS_PUBLIC_BASE_URL over forwarded headers for generate-pdf origin", async () => {
const { createJob } = await import("../../repositories/jobs");
const { generateFinalPdf } = await import("../../pipeline/index");
const job = await createJob({
source: "manual",
title: "Origin Test",
employer: "Example Co",
jobUrl: "https://example.com/job/origin-test",
jobDescription: "Test description",
});
const previousBaseUrl = process.env.JOBOPS_PUBLIC_BASE_URL;
process.env.JOBOPS_PUBLIC_BASE_URL = "https://canonical.jobops.example";
try {
const res = await fetch(`${baseUrl}/api/jobs/${job.id}/generate-pdf`, {
method: "POST",
headers: {
"x-forwarded-proto": "http",
"x-forwarded-host": "attacker.example",
},
});
expect(res.status).toBe(200);
expect(vi.mocked(generateFinalPdf)).toHaveBeenCalledWith(job.id, {
requestOrigin: "https://canonical.jobops.example",
});
} finally {
if (previousBaseUrl === undefined) {
delete process.env.JOBOPS_PUBLIC_BASE_URL;
} else {
process.env.JOBOPS_PUBLIC_BASE_URL = previousBaseUrl;
}
}
});
it("returns 409 when patching to a duplicate job URL", async () => {
const { createJob } = await import("../../repositories/jobs");
const first = await createJob({
source: "manual",
title: "First",
employer: "Acme",
jobUrl: "https://example.com/job/first",
jobDescription: "First description",
});
const second = await createJob({
source: "manual",
title: "Second",
employer: "Acme",
jobUrl: "https://example.com/job/second",
jobDescription: "Second description",
});
const res = await fetch(`${baseUrl}/api/jobs/${second.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jobUrl: first.jobUrl }),
});
const body = await res.json();
expect(res.status).toBe(409);
expect(body.ok).toBe(false);
expect(body.error.code).toBe("CONFLICT");
expect(typeof body.meta.requestId).toBe("string");
});
it("validates job updates and supports skip/delete flow", async () => {
const { createJob } = await import("../../repositories/jobs");
const job = await createJob({
source: "manual",
title: "Test Role",
employer: "Acme",
jobUrl: "https://example.com/job/2",
jobDescription: "Test description",
});
const badRes = await fetch(`${baseUrl}/api/jobs/${job.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ suitabilityScore: 1000 }),
});
const badBody = await badRes.json();
expect(badRes.status).toBe(400);
expect(badBody.ok).toBe(false);
expect(badBody.error.code).toBe("INVALID_REQUEST");
expect(typeof badBody.meta.requestId).toBe("string");
const invalidCoreRes = await fetch(`${baseUrl}/api/jobs/${job.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ employer: " " }),
});
const invalidCoreBody = await invalidCoreRes.json();
expect(invalidCoreRes.status).toBe(400);
expect(invalidCoreBody.ok).toBe(false);
expect(invalidCoreBody.error.code).toBe("INVALID_REQUEST");
expect(typeof invalidCoreBody.meta.requestId).toBe("string");
const patchRes = await fetch(`${baseUrl}/api/jobs/${job.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ suitabilityScore: 77 }),
});
const patchBody = await patchRes.json();
expect(patchRes.status).toBe(200);
expect(patchBody.ok).toBe(true);
expect(patchBody.data.suitabilityScore).toBe(77);
expect(typeof patchBody.meta.requestId).toBe("string");
const skipRes = await fetch(`${baseUrl}/api/jobs/actions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "skip", jobIds: [job.id] }),
});
const skipBody = await skipRes.json();
expect(skipBody.data.results).toHaveLength(1);
expect(skipBody.data.results[0].ok).toBe(true);
expect(skipBody.data.results[0].job.status).toBe("skipped");
const deleteRes = await fetch(`${baseUrl}/api/jobs/status/skipped`, {
method: "DELETE",
});
const deleteBody = await deleteRes.json();
expect(deleteBody.data.count).toBe(1);
});
it("runs skip action with partial failures", async () => {
const { createJob } = await import("../../repositories/jobs");
const discovered = await createJob({
source: "manual",
title: "Discovered Role",
employer: "Acme",
jobUrl: "https://example.com/job/action-discovered",
jobDescription: "Test description",
});
const ready = await createJob({
source: "manual",
title: "Ready Role",
employer: "Beta",
jobUrl: "https://example.com/job/action-ready",
jobDescription: "Test description",
});
const applied = await createJob({
source: "manual",
title: "Applied Role",
employer: "Gamma",
jobUrl: "https://example.com/job/action-applied",
jobDescription: "Test description",
});
const { updateJob } = await import("../../repositories/jobs");
await updateJob(ready.id, { status: "ready" });
await updateJob(applied.id, { status: "applied" });
const res = await fetch(`${baseUrl}/api/jobs/actions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "skip",
jobIds: [discovered.id, ready.id, applied.id, "missing-id"],
}),
});
const body = await res.json();
expect(res.status).toBe(200);
expect(body.ok).toBe(true);
expect(body.meta.requestId).toBeTruthy();
expect(body.data.requested).toBe(4);
expect(body.data.succeeded).toBe(2);
expect(body.data.failed).toBe(2);
const failures = body.data.results.filter((r: any) => !r.ok);
expect(failures).toHaveLength(2);
expect(failures.map((r: any) => r.error.code).sort()).toEqual([
"INVALID_REQUEST",
"NOT_FOUND",
]);
});
it("runs move_to_ready action and rejects ineligible statuses", async () => {
const { createJob, updateJob } = await import("../../repositories/jobs");
const discovered = await createJob({
source: "manual",
title: "New Role",
employer: "Acme",
jobUrl: "https://example.com/job/action-ready-1",
jobDescription: "Test description",
});
const ready = await createJob({
source: "manual",
title: "Already Ready",
employer: "Acme",
jobUrl: "https://example.com/job/action-ready-2",
jobDescription: "Test description",
});
await updateJob(ready.id, { status: "ready" });
const { processJob } = await import("../../pipeline/index");
const previousBaseUrl = process.env.JOBOPS_PUBLIC_BASE_URL;
process.env.JOBOPS_PUBLIC_BASE_URL = "https://canonical.jobops.example";
try {
const res = await fetch(`${baseUrl}/api/jobs/actions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "move_to_ready",
jobIds: [discovered.id, ready.id],
}),
});
const body = await res.json();
expect(body.ok).toBe(true);
expect(body.data.succeeded).toBe(1);
expect(body.data.failed).toBe(1);
expect(vi.mocked(processJob)).toHaveBeenCalledWith(discovered.id, {
force: false,
requestOrigin: "https://canonical.jobops.example",
});
expect(
body.data.results.find((r: any) => r.jobId === ready.id).error.code,
).toBe("INVALID_REQUEST");
} finally {
if (previousBaseUrl === undefined) {
delete process.env.JOBOPS_PUBLIC_BASE_URL;
} else {
process.env.JOBOPS_PUBLIC_BASE_URL = previousBaseUrl;
}
}
});
it("supports legacy move_to_ready endpoint", async () => {
const { createJob } = await import("../../repositories/jobs");
const { processJob } = await import("../../pipeline/index");
const job = await createJob({
source: "manual",
title: "Legacy Ready Route",
employer: "Acme",
jobUrl: "https://example.com/job/legacy-process-1",
jobDescription: "Test description",
});
const previousBaseUrl = process.env.JOBOPS_PUBLIC_BASE_URL;
process.env.JOBOPS_PUBLIC_BASE_URL = "https://canonical.jobops.example";
try {
const res = await fetch(`${baseUrl}/api/jobs/${job.id}/process`, {
method: "POST",
});
const body = await res.json();
expect(res.status).toBe(200);
expect(body.ok).toBe(true);
expect(vi.mocked(processJob)).toHaveBeenCalledWith(job.id, {
force: false,
requestOrigin: "https://canonical.jobops.example",
});
} finally {
if (previousBaseUrl === undefined) {
delete process.env.JOBOPS_PUBLIC_BASE_URL;
} else {
process.env.JOBOPS_PUBLIC_BASE_URL = previousBaseUrl;
}
}
});
it("runs rescore action with partial failures", async () => {
const { createJob, updateJob } = await import("../../repositories/jobs");
const { scoreJobSuitability } = await import("../../services/scorer");
const { getProfile } = await import("../../services/profile");
vi.mocked(getProfile).mockResolvedValue({});
vi.mocked(scoreJobSuitability).mockResolvedValue({
score: 81,
reason: "Updated fit from action rescore",
});
const discovered = await createJob({
source: "manual",
title: "Discovered Role",
employer: "Acme",
jobUrl: "https://example.com/job/action-rescore-1",
jobDescription: "Test description",
});
const ready = await createJob({
source: "manual",
title: "Ready Role",
employer: "Beta",
jobUrl: "https://example.com/job/action-rescore-2",
jobDescription: "Test description",
});
const processing = await createJob({
source: "manual",
title: "Processing Role",
employer: "Gamma",
jobUrl: "https://example.com/job/action-rescore-3",
jobDescription: "Test description",
});
await updateJob(ready.id, { status: "ready" });
await updateJob(processing.id, { status: "processing" });
const res = await fetch(`${baseUrl}/api/jobs/actions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "rescore",
jobIds: [discovered.id, ready.id, processing.id, "missing-id"],
}),
});
const body = await res.json();
expect(res.status).toBe(200);
expect(body.ok).toBe(true);
expect(body.meta.requestId).toBeTruthy();
expect(body.data.requested).toBe(4);
expect(body.data.succeeded).toBe(2);
expect(body.data.failed).toBe(2);
expect(
body.data.results.find((r: any) => r.jobId === discovered.id).job
.suitabilityScore,
).toBe(81);
expect(
body.data.results.find((r: any) => r.jobId === ready.id).job
.suitabilityScore,
).toBe(81);
expect(
body.data.results.find((r: any) => r.jobId === processing.id).error.code,
).toBe("INVALID_REQUEST");
expect(
body.data.results.find((r: any) => r.jobId === "missing-id").error.code,
).toBe("NOT_FOUND");
expect(vi.mocked(getProfile)).toHaveBeenCalledTimes(1);
});
it("streams job action progress with done counters", async () => {
const { createJob, updateJob } = await import("../../repositories/jobs");
const discovered = await createJob({
source: "manual",
title: "Discovered Role",
employer: "Acme",
jobUrl: "https://example.com/job/action-stream-1",
jobDescription: "Test description",
});
const ready = await createJob({
source: "manual",
title: "Ready Role",
employer: "Beta",
jobUrl: "https://example.com/job/action-stream-2",
jobDescription: "Test description",
});
const applied = await createJob({
source: "manual",
title: "Applied Role",
employer: "Gamma",
jobUrl: "https://example.com/job/action-stream-3",
jobDescription: "Test description",
});
await updateJob(ready.id, { status: "ready" });
await updateJob(applied.id, { status: "applied" });
const res = await fetch(`${baseUrl}/api/jobs/actions/stream`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "skip",
jobIds: [discovered.id, ready.id, applied.id],
}),
});
expect(res.status).toBe(200);
expect(res.headers.get("content-type")).toContain("text/event-stream");
const reader = res.body?.getReader();
expect(reader).toBeDefined();
if (!reader) return;
const decoder = new TextDecoder();
const events: any[] = [];
let buffer = "";
let hasCompleted = false;
try {
while (!hasCompleted) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let separatorIndex = buffer.indexOf("\n\n");
while (separatorIndex !== -1) {
const frame = buffer.slice(0, separatorIndex);
buffer = buffer.slice(separatorIndex + 2);
const dataLines = frame
.split("\n")
.filter((line) => line.startsWith("data:"))
.map((line) => line.slice(5).trim())
.filter(Boolean);
for (const line of dataLines) {
const event = JSON.parse(line);
events.push(event);
if (event.type === "completed") {
hasCompleted = true;
}
}
separatorIndex = buffer.indexOf("\n\n");
}
}
} finally {
await reader.cancel();
}
expect(events[0].type).toBe("started");
expect(events[0].completed).toBe(0);
expect(events[0].requested).toBe(3);
expect(events.filter((event) => event.type === "progress")).toHaveLength(3);
expect(events.at(-1)?.type).toBe("completed");
expect(events.at(-1)?.completed).toBe(3);
expect(events.at(-1)?.succeeded).toBe(2);
expect(events.at(-1)?.failed).toBe(1);
});
it("validates job action payloads", async () => {
const tooManyIds = Array.from(
{ length: 101 },
(_, index) => `job-${index}`,
);
const res = await fetch(`${baseUrl}/api/jobs/actions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "skip",
jobIds: tooManyIds,
}),
});
const body = await res.json();
expect(res.status).toBe(400);
expect(body.ok).toBe(false);
expect(body.error.code).toBe("INVALID_REQUEST");
expect(body.meta.requestId).toBeTruthy();
});
it("applies a job", async () => {
const { createJob } = await import("../../repositories/jobs");
const job = await createJob({
source: "manual",
title: "Test Role",
employer: "Acme",
jobUrl: "https://example.com/job/3",
jobDescription: "Test description",
});
const res = await fetch(`${baseUrl}/api/jobs/${job.id}/apply`, {
method: "POST",
});
const body = await res.json();
expect(body.ok).toBe(true);
expect(body.data.status).toBe("applied");
expect(body.data.appliedAt).toBeTruthy();
});
it("rescoring a job updates the suitability fields", async () => {
const { createJob } = await import("../../repositories/jobs");
const { scoreJobSuitability } = await import("../../services/scorer");
const { getProfile } = await import("../../services/profile");
vi.mocked(getProfile).mockResolvedValue({});
vi.mocked(scoreJobSuitability).mockResolvedValue({
score: 77,
reason: "Updated fit",
});
const job = await createJob({
source: "manual",
title: "Test Role",
employer: "Acme",
jobUrl: "https://example.com/job/5",
jobDescription: "Test description",
});
const { updateJob } = await import("../../repositories/jobs");
await updateJob(job.id, {
suitabilityScore: 55,
suitabilityReason: "Old fit",
});
const res = await fetch(`${baseUrl}/api/jobs/actions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "rescore", jobIds: [job.id] }),
});
const body = await res.json();
expect(body.ok).toBe(true);
expect(body.data.results).toHaveLength(1);
expect(body.data.results[0].ok).toBe(true);
expect(body.data.results[0].job.suitabilityScore).toBe(77);
expect(body.data.results[0].job.suitabilityReason).toBe("Updated fit");
});
it("deletes jobs below a score threshold (excluding applied)", async () => {
const { createJob, updateJob } = await import("../../repositories/jobs");
// Create jobs with different scores and statuses
const lowScoreJob = await createJob({
source: "manual",
title: "Low Score Job",
employer: "Company A",
jobUrl: "https://example.com/job/low",
jobDescription: "Test description",
});
await updateJob(lowScoreJob.id, { suitabilityScore: 30 });
const mediumScoreJob = await createJob({
source: "manual",
title: "Medium Score Job",
employer: "Company B",
jobUrl: "https://example.com/job/medium",
jobDescription: "Test description",
});
await updateJob(mediumScoreJob.id, { suitabilityScore: 60 });
const boundaryScoreJob = await createJob({
source: "manual",
title: "Boundary Score Job",
employer: "Company Boundary",
jobUrl: "https://example.com/job/boundary",
jobDescription: "Test description",
});
await updateJob(boundaryScoreJob.id, { suitabilityScore: 50 });
const highScoreJob = await createJob({
source: "manual",
title: "High Score Job",
employer: "Company C",
jobUrl: "https://example.com/job/high",
jobDescription: "Test description",
});
await updateJob(highScoreJob.id, { suitabilityScore: 90 });
const appliedLowScoreJob = await createJob({
source: "manual",
title: "Applied Low Score Job",
employer: "Company D",
jobUrl: "https://example.com/job/applied-low",
jobDescription: "Test description",
});
await updateJob(appliedLowScoreJob.id, {
suitabilityScore: 30,
status: "applied",
});
// Delete jobs below score 50
const deleteRes = await fetch(`${baseUrl}/api/jobs/score/50`, {
method: "DELETE",
});
const deleteBody = await deleteRes.json();
expect(deleteBody.ok).toBe(true);
expect(deleteBody.data.count).toBe(1);
expect(deleteBody.data.threshold).toBe(50);
// Verify only the low score non-applied job was deleted
const listRes = await fetch(`${baseUrl}/api/jobs`);
const listBody = await listRes.json();
const remainingJobIds = listBody.data.jobs.map((j: any) => j.id);
expect(remainingJobIds).not.toContain(lowScoreJob.id);
expect(remainingJobIds).toContain(boundaryScoreJob.id);
expect(remainingJobIds).toContain(mediumScoreJob.id);
expect(remainingJobIds).toContain(highScoreJob.id);
expect(remainingJobIds).toContain(appliedLowScoreJob.id); // Applied job preserved
});
it("rejects invalid score thresholds", async () => {
// Test invalid threshold (above 100)
const invalidRes = await fetch(`${baseUrl}/api/jobs/score/150`, {
method: "DELETE",
});
expect(invalidRes.status).toBe(400);
const invalidBody = await invalidRes.json();
expect(invalidBody.ok).toBe(false);
expect(invalidBody.error.code).toBe("INVALID_REQUEST");
// Test invalid threshold (below 0)
const negativeRes = await fetch(`${baseUrl}/api/jobs/score/-10`, {
method: "DELETE",
});
expect(negativeRes.status).toBe(400);
// Test non-numeric threshold
const nanRes = await fetch(`${baseUrl}/api/jobs/score/abc`, {
method: "DELETE",
});
expect(nanRes.status).toBe(400);
});
it("checks visa sponsor status for a job", async () => {
const { searchSponsors } = await import(
"../../services/visa-sponsors/index"
);
vi.mocked(searchSponsors).mockReturnValue([
{
sponsor: { organisationName: "ACME CORP SPONSOR" } as any,
score: 100,
matchedName: "acme corp sponsor",
},
]);
const { createJob } = await import("../../repositories/jobs");
const job = await createJob({
source: "manual",
title: "Sponsored Dev",
employer: "Acme",
jobUrl: "https://example.com/job/4",
});
const res = await fetch(`${baseUrl}/api/jobs/${job.id}/check-sponsor`, {
method: "POST",
});
const body = await res.json();
expect(body.ok).toBe(true);
expect(body.data.sponsorMatchScore).toBe(100);
expect(body.data.sponsorMatchNames).toContain("ACME CORP SPONSOR");
});
describe("Application Tracking", () => {
let jobId: string;
beforeEach(async () => {
const { createJob } = await import("../../repositories/jobs");
const job = await createJob({
source: "manual",
title: "Tracking Test",
employer: "Test Corp",
jobUrl: "https://example.com/tracking",
});
jobId = job.id;
});
it("transitions stages and retrieves events", async () => {
// 1. Initial transition to applied
const trans1 = await fetch(`${baseUrl}/api/jobs/${jobId}/stages`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ toStage: "applied" }),
});
const body1 = await trans1.json();
expect(body1.ok).toBe(true);
expect(body1.data.toStage).toBe("applied");
const eventId = body1.data.id;
// 2. Transition to recruiter_screen with metadata
await fetch(`${baseUrl}/api/jobs/${jobId}/stages`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
toStage: "recruiter_screen",
metadata: { note: "Called by recruiter" },
}),
});
// 3. Get events
const eventsRes = await fetch(`${baseUrl}/api/jobs/${jobId}/events`);
const eventsBody = await eventsRes.json();
expect(eventsBody.ok).toBe(true);
expect(eventsBody.data).toHaveLength(2);
expect(eventsBody.data[0].toStage).toBe("applied");
expect(eventsBody.data[1].toStage).toBe("recruiter_screen");
expect(eventsBody.data[1].metadata.note).toBe("Called by recruiter");
// 4. Patch an event
const patchRes = await fetch(
`${baseUrl}/api/jobs/${jobId}/events/${eventId}`,
{
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ metadata: { note: "Updated note" } }),
},
);
expect(patchRes.status).toBe(200);
const eventsRes2 = await fetch(`${baseUrl}/api/jobs/${jobId}/events`);
const eventsBody2 = await eventsRes2.json();
expect(eventsBody2.data[0].metadata.note).toBe("Updated note");
// 5. Delete an event
const deleteRes = await fetch(
`${baseUrl}/api/jobs/${jobId}/events/${eventId}`,
{
method: "DELETE",
},
);
expect(deleteRes.status).toBe(200);
const eventsRes3 = await fetch(`${baseUrl}/api/jobs/${jobId}/events`);
const eventsBody3 = await eventsRes3.json();
expect(eventsBody3.data).toHaveLength(1);
});
it("manages application tasks", async () => {
const { db, schema } = await import("../../db/index");
const { eq } = await import("drizzle-orm");
const { tasks } = schema;
// 1. Initial state
const res1 = await fetch(`${baseUrl}/api/jobs/${jobId}/tasks`);
const body1 = await res1.json();
expect(body1.ok).toBe(true);
expect(body1.data).toEqual([]);
// 2. Insert a task
await (db as any)
.insert(tasks)
.values({
id: "task-1",
applicationId: jobId,
type: "todo",
title: "Complete test task",
isCompleted: false,
})
.run();
const res2 = await fetch(`${baseUrl}/api/jobs/${jobId}/tasks`);
const body2 = await res2.json();
expect(body2.data).toHaveLength(1);
expect(body2.data[0].title).toBe("Complete test task");
// 3. Test filtering (completed vs non-completed)
await (db as any)
.update(tasks)
.set({ isCompleted: true })
.where(eq(tasks.id, "task-1"))
.run();
const res3 = await fetch(`${baseUrl}/api/jobs/${jobId}/tasks`);
const body3 = await res3.json();
expect(body3.data).toHaveLength(0); // includeCompleted defaults to false
const res4 = await fetch(
`${baseUrl}/api/jobs/${jobId}/tasks?includeCompleted=true`,
);
const body4 = await res4.json();
expect(body4.data).toHaveLength(1);
});
it("updates job outcome", async () => {
const res = await fetch(`${baseUrl}/api/jobs/${jobId}/outcome`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ outcome: "rejected" }),
});
const body = await res.json();
expect(body.ok).toBe(true);
expect(body.data.outcome).toBe("rejected");
expect(body.data.closedAt).toBeTruthy();
});
});
});