Deduplicate shared helpers and enforce aliased imports (#228)

* Deduplicate string cleanup helpers and not-found responses

* Enforce aliased imports for infra and shared modules

* Enforce @client/@server aliases for deep relative imports

* Deduplicate visa sponsor and location filter definitions

* Use shared city filter export in extractor location checks
This commit is contained in:
Shaheer Sarfaraz 2026-02-22 16:13:52 +00:00 committed by GitHub
parent 16acdf2b5e
commit 3da5ea35b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
67 changed files with 385 additions and 346 deletions

View File

@ -17,6 +17,66 @@
"tailwindDirectives": true
}
},
"linter": {
"rules": {
"style": {
"noRestrictedImports": {
"level": "error",
"options": {
"patterns": [
{
"group": [
"../infra/**",
"../../infra/**",
"../../../infra/**",
"../../../../infra/**",
"../shared/**",
"../../shared/**",
"../../../shared/**",
"../../../../shared/**"
],
"message": "Use path aliases (for example @infra/* or @shared/*) instead of parent-relative imports for shared modules."
},
{
"group": [
"../../api",
"../../api/**",
"../../hooks",
"../../hooks/**",
"../../components",
"../../components/**",
"../../lib",
"../../lib/**",
"../../test",
"../../test/**"
],
"message": "Use @client/* aliases instead of ../../ imports for client modules."
},
{
"group": [
"../../services",
"../../services/**",
"../../repositories",
"../../repositories/**",
"../../config",
"../../config/**",
"../../utils",
"../../utils/**",
"../../pipeline",
"../../pipeline/**",
"../../db",
"../../db/**",
"../../extractors",
"../../extractors/**"
],
"message": "Use @server/* aliases instead of ../../ imports for server modules."
}
]
}
}
}
}
},
"overrides": [
{
"includes": ["**/*.test.ts", "**/*.test.tsx", "**/test-utils.ts"],

View File

@ -63,13 +63,6 @@ export interface AdzunaResult {
error?: string;
}
export function shouldApplyStrictLocationFilter(
location: string,
countryKey: string,
): boolean {
return shouldApplyStrictCityFilter(location, countryKey);
}
function resolveTsxCliPath(): string | null {
try {
return require.resolve("tsx/dist/cli.mjs");
@ -214,8 +207,7 @@ export async function runAdzuna(
for (let runIndex = 0; runIndex < runLocations.length; runIndex += 1) {
const location = runLocations[runIndex];
const strictLocationFilter =
location !== null &&
shouldApplyStrictLocationFilter(location, countryKey);
location !== null && shouldApplyStrictCityFilter(location, countryKey);
await new Promise<void>((resolve, reject) => {
const extractorEnv = {

View File

@ -1,15 +1,13 @@
import { shouldApplyStrictCityFilter } from "@shared/search-cities.js";
import { describe, expect, it } from "vitest";
import { shouldApplyStrictLocationFilter } from "../src/run";
describe("adzuna location query strictness", () => {
it("enables strict filtering when city differs from country", () => {
expect(shouldApplyStrictLocationFilter("Leeds", "united kingdom")).toBe(
true,
);
expect(shouldApplyStrictCityFilter("Leeds", "united kingdom")).toBe(true);
});
it("disables strict filtering when location is country-level", () => {
expect(shouldApplyStrictLocationFilter("UK", "united kingdom")).toBe(false);
expect(shouldApplyStrictLocationFilter("United States", "us")).toBe(false);
expect(shouldApplyStrictCityFilter("UK", "united kingdom")).toBe(false);
expect(shouldApplyStrictCityFilter("United States", "us")).toBe(false);
});
});

View File

@ -65,13 +65,6 @@ export interface HiringCafeResult {
error?: string;
}
export function shouldApplyStrictLocationFilter(
location: string,
countryKey: string,
): boolean {
return shouldApplyStrictCityFilter(location, countryKey);
}
function resolveTsxCliPath(): string | null {
try {
return require.resolve("tsx/dist/cli.mjs");
@ -213,8 +206,7 @@ export async function runHiringCafe(
for (let runIndex = 0; runIndex < runLocations.length; runIndex += 1) {
const location = runLocations[runIndex];
const strictLocationFilter =
location !== null &&
shouldApplyStrictLocationFilter(location, countryKey);
location !== null && shouldApplyStrictCityFilter(location, countryKey);
await clearStorageDataset();

View File

@ -1,15 +1,13 @@
import { shouldApplyStrictCityFilter } from "@shared/search-cities.js";
import { describe, expect, it } from "vitest";
import { shouldApplyStrictLocationFilter } from "../src/run";
describe("hiringcafe location query strictness", () => {
it("enables strict filtering when city differs from country", () => {
expect(shouldApplyStrictLocationFilter("Leeds", "united kingdom")).toBe(
true,
);
expect(shouldApplyStrictCityFilter("Leeds", "united kingdom")).toBe(true);
});
it("disables strict filtering when location is country-level", () => {
expect(shouldApplyStrictLocationFilter("UK", "united kingdom")).toBe(false);
expect(shouldApplyStrictLocationFilter("United States", "us")).toBe(false);
expect(shouldApplyStrictCityFilter("UK", "united kingdom")).toBe(false);
expect(shouldApplyStrictCityFilter("United States", "us")).toBe(false);
});
});

View File

@ -1,3 +1,5 @@
import * as api from "@client/api";
import { renderWithQueryClient } from "@client/test/renderWithQueryClient";
import { createJob } from "@shared/testing/factories.js";
import type { Job } from "@shared/types.js";
import { fireEvent, screen, waitFor } from "@testing-library/react";
@ -5,8 +7,6 @@ import type React from "react";
import { MemoryRouter } from "react-router-dom";
import { toast } from "sonner";
import { beforeEach, describe, expect, it, vi } from "vitest";
import * as api from "../../api";
import { renderWithQueryClient } from "../../test/renderWithQueryClient";
import { DiscoveredPanel } from "./DiscoveredPanel";
const render = (ui: Parameters<typeof renderWithQueryClient>[0]) =>
@ -44,11 +44,11 @@ vi.mock("@/components/ui/dropdown-menu", () => {
};
});
vi.mock("../../hooks/useSettings", () => ({
vi.mock("@client/hooks/useSettings", () => ({
useSettings: () => ({ showSponsorInfo: false }),
}));
vi.mock("../../api", () => ({
vi.mock("@client/api", () => ({
rescoreJob: vi.fn(),
skipJob: vi.fn(),
processJob: vi.fn(),

View File

@ -1,10 +1,10 @@
import * as api from "@client/api";
import { useSkipJobMutation } from "@client/hooks/queries/useJobMutations";
import { useRescoreJob } from "@client/hooks/useRescoreJob";
import type { Job } from "@shared/types.js";
import type React from "react";
import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import * as api from "../../api";
import { useSkipJobMutation } from "../../hooks/queries/useJobMutations";
import { useRescoreJob } from "../../hooks/useRescoreJob";
import { JobDetailsEditDrawer } from "../JobDetailsEditDrawer";
import { DecideMode } from "./DecideMode";
import { EmptyState } from "./EmptyState";

View File

@ -1,24 +1,24 @@
import * as api from "@client/api";
import { useProfile } from "@client/hooks/useProfile";
import { _resetTracerReadinessCache } from "@client/hooks/useTracerReadiness";
import { renderWithQueryClient } from "@client/test/renderWithQueryClient";
import { createJob as createBaseJob } from "@shared/testing/factories.js";
import type { Job } from "@shared/types.js";
import { fireEvent, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import * as api from "../../api";
import { useProfile } from "../../hooks/useProfile";
import { _resetTracerReadinessCache } from "../../hooks/useTracerReadiness";
import { renderWithQueryClient } from "../../test/renderWithQueryClient";
import { TailorMode } from "./TailorMode";
const render = (ui: Parameters<typeof renderWithQueryClient>[0]) =>
renderWithQueryClient(ui);
vi.mock("../../api", () => ({
vi.mock("@client/api", () => ({
getResumeProjectsCatalog: vi.fn().mockResolvedValue([]),
updateJob: vi.fn(),
summarizeJob: vi.fn(),
getTracerReadiness: vi.fn(),
}));
vi.mock("../../hooks/useProfile", () => ({
vi.mock("@client/hooks/useProfile", () => ({
useProfile: vi.fn(),
}));

View File

@ -1,8 +1,8 @@
import * as api from "@client/api";
import type { Job, JobChatMessage, JobChatStreamEvent } from "@shared/types";
import type React from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import * as api from "../../api";
import { Composer } from "./Composer";
import { MessageList } from "./MessageList";

View File

@ -1,3 +1,6 @@
import * as api from "@client/api";
import { useProfile } from "@client/hooks/useProfile";
import { useTracerReadiness } from "@client/hooks/useTracerReadiness";
import type { Job } from "@shared/types.js";
import { ArrowLeft, Check, FileText, Loader2, Sparkles } from "lucide-react";
import type React from "react";
@ -6,9 +9,6 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import * as api from "../../api";
import { useProfile } from "../../hooks/useProfile";
import { useTracerReadiness } from "../../hooks/useTracerReadiness";
import {
fromEditableSkillGroups,
getOriginalHeadline,

View File

@ -1,6 +1,6 @@
import * as api from "@client/api";
import type { Job, ResumeProjectCatalogItem } from "@shared/types.js";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import * as api from "../../api";
import {
createTailoredSkillDraftId,
type EditableSkillGroup,

View File

@ -1,3 +1,4 @@
import { CollapsibleSection } from "@client/components/discovered-panel/CollapsibleSection";
import {
type ApplicationStage,
STAGE_LABELS,
@ -18,7 +19,6 @@ import {
import React from "react";
import { Badge } from "@/components/ui/badge";
import { cn, formatTimestamp, formatTimestampWithTime } from "@/lib/utils";
import { CollapsibleSection } from "../../components/discovered-panel/CollapsibleSection";
const stageIcons: Record<ApplicationStage, React.ReactNode> = {
applied: <CheckCircle2 className="h-4 w-4" />,

View File

@ -1,10 +1,10 @@
import * as api from "@client/api";
import { renderWithQueryClient } from "@client/test/renderWithQueryClient";
import { createJob } from "@shared/testing/factories.js";
import type { Job } from "@shared/types.js";
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
import type React from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import * as api from "../../api";
import { renderWithQueryClient } from "../../test/renderWithQueryClient";
import { JobDetailPanel } from "./JobDetailPanel";
const render = (ui: Parameters<typeof renderWithQueryClient>[0]) =>
@ -42,7 +42,7 @@ vi.mock("@/components/ui/dropdown-menu", () => {
};
});
vi.mock("../../components", () => ({
vi.mock("@client/components", () => ({
DiscoveredPanel: ({ job }: { job: Job | null }) => (
<div data-testid="discovered-panel">{job?.id ?? "no-job"}</div>
),
@ -51,7 +51,7 @@ vi.mock("../../components", () => ({
TailoredSummary: () => <div data-testid="tailored-summary" />,
}));
vi.mock("../../components/ReadyPanel", () => ({
vi.mock("@client/components/ReadyPanel", () => ({
ReadyPanel: ({ onEditDescription }: { onEditDescription?: () => void }) => (
<div>
<div data-testid="ready-panel" />
@ -62,7 +62,7 @@ vi.mock("../../components/ReadyPanel", () => ({
),
}));
vi.mock("../../components/TailoringEditor", () => ({
vi.mock("@client/components/TailoringEditor", () => ({
TailoringEditor: ({
onDirtyChange,
}: {
@ -79,7 +79,7 @@ vi.mock("../../components/TailoringEditor", () => ({
),
}));
vi.mock("../../components/JobDetailsEditDrawer", () => ({
vi.mock("@client/components/JobDetailsEditDrawer", () => ({
JobDetailsEditDrawer: ({
open,
onOpenChange,
@ -116,7 +116,7 @@ vi.mock("@/lib/utils", async (importOriginal) => {
};
});
vi.mock("../../api", () => ({
vi.mock("@client/api", () => ({
updateJob: vi.fn(),
processJob: vi.fn(),
generateJobPdf: vi.fn(),

View File

@ -1,3 +1,18 @@
import * as api from "@client/api";
import {
DiscoveredPanel,
FitAssessment,
JobHeader,
TailoredSummary,
} from "@client/components";
import { JobDetailsEditDrawer } from "@client/components/JobDetailsEditDrawer";
import { ReadyPanel } from "@client/components/ReadyPanel";
import { TailoringEditor } from "@client/components/TailoringEditor";
import {
useMarkAsAppliedMutation,
useSkipJobMutation,
} from "@client/hooks/queries/useJobMutations";
import { useProfile } from "@client/hooks/useProfile";
import type { Job, JobListItem } from "@shared/types.js";
import {
CheckCircle2,
@ -32,21 +47,6 @@ import {
safeFilenamePart,
stripHtml,
} from "@/lib/utils";
import * as api from "../../api";
import {
DiscoveredPanel,
FitAssessment,
JobHeader,
TailoredSummary,
} from "../../components";
import { JobDetailsEditDrawer } from "../../components/JobDetailsEditDrawer";
import { ReadyPanel } from "../../components/ReadyPanel";
import { TailoringEditor } from "../../components/TailoringEditor";
import {
useMarkAsAppliedMutation,
useSkipJobMutation,
} from "../../hooks/queries/useJobMutations";
import { useProfile } from "../../hooks/useProfile";
import type { FilterTab } from "./constants";
interface JobDetailPanelProps {

View File

@ -1,6 +1,6 @@
import { PipelineProgress } from "@client/components";
import type { JobStatus } from "@shared/types.js";
import type React from "react";
import { PipelineProgress } from "../../components";
interface OrchestratorSummaryProps {
stats: Record<JobStatus, number>;

View File

@ -1,12 +1,12 @@
import * as api from "@client/api";
import { createJob } from "@shared/testing/factories.js";
import type { JobActionResponse, JobActionStreamEvent } from "@shared/types.js";
import { act, renderHook, waitFor } from "@testing-library/react";
import { toast } from "sonner";
import { beforeEach, describe, expect, it, vi } from "vitest";
import * as api from "../../api";
import { useJobSelectionActions } from "./useJobSelectionActions";
vi.mock("../../api", () => ({
vi.mock("@client/api", () => ({
streamJobAction: vi.fn(),
}));

View File

@ -1,3 +1,4 @@
import * as api from "@client/api";
import type {
JobAction,
JobActionResponse,
@ -5,7 +6,6 @@ import type {
} from "@shared/types.js";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import * as api from "../../api";
import type { FilterTab } from "./constants";
import { JobActionProgressToast } from "./JobActionProgressToast";
import {

View File

@ -1,13 +1,13 @@
import * as api from "@client/api";
import { renderHookWithQueryClient } from "@client/test/renderWithQueryClient";
import { act, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import * as api from "../../api";
import { renderHookWithQueryClient } from "../../test/renderWithQueryClient";
import { useOrchestratorData } from "./useOrchestratorData";
const renderHook = (callback: () => ReturnType<typeof useOrchestratorData>) =>
renderHookWithQueryClient(callback);
vi.mock("../../api", () => ({
vi.mock("@client/api", () => ({
getJobs: vi.fn(),
getJobsRevision: vi.fn(),
getJob: vi.fn(),

View File

@ -1,10 +1,10 @@
import * as api from "@client/api";
import { subscribeToEventSource } from "@client/lib/sse";
import type { Job, JobListItem, JobStatus } from "@shared/types";
import { useQueryClient } from "@tanstack/react-query";
import { useCallback, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { queryKeys } from "@/client/lib/queryKeys";
import * as api from "../../api";
import { subscribeToEventSource } from "../../lib/sse";
const initialStats: Record<JobStatus, number> = {
discovered: 0,

View File

@ -3,6 +3,7 @@ import {
sourceLabel as getExtractorSourceLabel,
} from "@shared/extractors";
import type { Job } from "@shared/types";
import { stripHtmlTags } from "@shared/utils/string";
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
@ -101,11 +102,7 @@ export async function copyTextToClipboard(text: string) {
}
// --- Text Processing ---
export const stripHtml = (value: string) =>
value
.replace(/<[^>]*>/g, " ")
.replace(/\s+/g, " ")
.trim();
export const stripHtml = (value: string) => stripHtmlTags(value);
export const safeFilenamePart = (value: string) => {
const cleaned = value.replace(/[^a-z0-9]/gi, "_");

View File

@ -1,4 +1,7 @@
import { notFound } from "@infra/errors";
import { fail } from "@infra/http";
import { logger } from "@infra/logger";
import { isDemoMode, sendDemoBlocked } from "@server/config/demo";
import {
createBackup,
deleteBackup,
@ -6,7 +9,6 @@ import {
listBackups,
} from "@server/services/backup/index";
import { type Request, type Response, Router } from "express";
import { isDemoMode, sendDemoBlocked } from "../../config/demo";
export const backupRouter = Router();
@ -104,7 +106,7 @@ backupRouter.delete("/:filename", async (req: Request, res: Response) => {
});
if (message.includes("not found")) {
res.status(404).json({ success: false, error: message });
return fail(res, notFound(message));
} else if (message.includes("Invalid")) {
res.status(400).json({ success: false, error: message });
} else {

View File

@ -17,7 +17,7 @@ describe.sequential("Database API routes", () => {
});
it("clears jobs and pipeline runs", async () => {
const { createJob } = await import("../../repositories/jobs");
const { createJob } = await import("@server/repositories/jobs");
await createJob({
source: "manual",
title: "Cleanup Role",

View File

@ -1,6 +1,6 @@
import { isDemoMode, sendDemoBlocked } from "@server/config/demo";
import { clearDatabase } from "@server/db/clear";
import { type Request, type Response, Router } from "express";
import { isDemoMode, sendDemoBlocked } from "../../config/demo";
import { clearDatabase } from "../../db/clear";
export const databaseRouter = Router();

View File

@ -1,6 +1,6 @@
import { ok } from "@infra/http";
import { getDemoInfo } from "@server/config/demo";
import { type Request, type Response, Router } from "express";
import { getDemoInfo } from "../../config/demo";
export const demoRouter = Router();

View File

@ -2,7 +2,7 @@ import type { Server } from "node:http";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { startServer, stopServer } from "./test-utils";
vi.mock("../../services/ghostwriter", () => ({
vi.mock("@server/services/ghostwriter", () => ({
listThreads: vi.fn(async () => [
{
id: "thread-1",

View File

@ -2,9 +2,9 @@ import { asyncRoute, fail, ok } from "@infra/http";
import { runWithRequestContext } from "@infra/request-context";
import { setupSse, writeSseData } from "@infra/sse";
import { badRequest, toAppError } from "@server/infra/errors";
import * as ghostwriterService from "@server/services/ghostwriter";
import { type Request, Router } from "express";
import { z } from "zod";
import * as ghostwriterService from "../../services/ghostwriter";
export const ghostwriterRouter = Router({ mergeParams: true });

View File

@ -17,7 +17,7 @@ describe.sequential("Jobs API routes", () => {
});
it("lists jobs and supports status filtering", async () => {
const { createJob } = await import("../../repositories/jobs");
const { createJob } = await import("@server/repositories/jobs");
const job = await createJob({
source: "manual",
title: "Test Role",
@ -40,7 +40,7 @@ describe.sequential("Jobs API routes", () => {
});
it("supports lightweight and full jobs list views", async () => {
const { createJob } = await import("../../repositories/jobs");
const { createJob } = await import("@server/repositories/jobs");
await createJob({
source: "manual",
title: "List View Role",
@ -76,7 +76,7 @@ describe.sequential("Jobs API routes", () => {
});
it("returns jobs revision and supports status filtering", async () => {
const { createJob, updateJob } = await import("../../repositories/jobs");
const { createJob, updateJob } = await import("@server/repositories/jobs");
const readyJob = await createJob({
source: "manual",
title: "Ready Role",
@ -133,7 +133,7 @@ describe.sequential("Jobs API routes", () => {
});
it("updates core job detail fields", async () => {
const { createJob } = await import("../../repositories/jobs");
const { createJob } = await import("@server/repositories/jobs");
const job = await createJob({
source: "manual",
title: "Original Title",
@ -176,7 +176,7 @@ describe.sequential("Jobs API routes", () => {
});
it("blocks enabling tracer links when readiness check fails", async () => {
const { createJob } = await import("../../repositories/jobs");
const { createJob } = await import("@server/repositories/jobs");
const job = await createJob({
source: "manual",
title: "Tracer Blocked",
@ -221,8 +221,8 @@ describe.sequential("Jobs API routes", () => {
});
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 { createJob } = await import("@server/repositories/jobs");
const { updateJob } = await import("@server/repositories/jobs");
const job = await createJob({
source: "manual",
title: "Tracer Already On",
@ -288,8 +288,8 @@ describe.sequential("Jobs API routes", () => {
});
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 { createJob } = await import("@server/repositories/jobs");
const { generateFinalPdf } = await import("@server/pipeline/index");
const job = await createJob({
source: "manual",
title: "Origin Test",
@ -324,7 +324,7 @@ describe.sequential("Jobs API routes", () => {
});
it("returns 409 when patching to a duplicate job URL", async () => {
const { createJob } = await import("../../repositories/jobs");
const { createJob } = await import("@server/repositories/jobs");
const first = await createJob({
source: "manual",
title: "First",
@ -354,7 +354,7 @@ describe.sequential("Jobs API routes", () => {
});
it("validates job updates and supports skip/delete flow", async () => {
const { createJob } = await import("../../repositories/jobs");
const { createJob } = await import("@server/repositories/jobs");
const job = await createJob({
source: "manual",
title: "Test Role",
@ -414,7 +414,7 @@ describe.sequential("Jobs API routes", () => {
});
it("runs skip action with partial failures", async () => {
const { createJob } = await import("../../repositories/jobs");
const { createJob } = await import("@server/repositories/jobs");
const discovered = await createJob({
source: "manual",
title: "Discovered Role",
@ -436,7 +436,7 @@ describe.sequential("Jobs API routes", () => {
jobUrl: "https://example.com/job/action-applied",
jobDescription: "Test description",
});
const { updateJob } = await import("../../repositories/jobs");
const { updateJob } = await import("@server/repositories/jobs");
await updateJob(ready.id, { status: "ready" });
await updateJob(applied.id, { status: "applied" });
@ -465,7 +465,7 @@ describe.sequential("Jobs API routes", () => {
});
it("runs move_to_ready action and rejects ineligible statuses", async () => {
const { createJob, updateJob } = await import("../../repositories/jobs");
const { createJob, updateJob } = await import("@server/repositories/jobs");
const discovered = await createJob({
source: "manual",
title: "New Role",
@ -481,7 +481,7 @@ describe.sequential("Jobs API routes", () => {
jobDescription: "Test description",
});
await updateJob(ready.id, { status: "ready" });
const { processJob } = await import("../../pipeline/index");
const { processJob } = await import("@server/pipeline/index");
const previousBaseUrl = process.env.JOBOPS_PUBLIC_BASE_URL;
process.env.JOBOPS_PUBLIC_BASE_URL = "https://canonical.jobops.example";
@ -516,8 +516,8 @@ describe.sequential("Jobs API routes", () => {
});
it("supports legacy move_to_ready endpoint", async () => {
const { createJob } = await import("../../repositories/jobs");
const { processJob } = await import("../../pipeline/index");
const { createJob } = await import("@server/repositories/jobs");
const { processJob } = await import("@server/pipeline/index");
const job = await createJob({
source: "manual",
title: "Legacy Ready Route",
@ -550,9 +550,9 @@ describe.sequential("Jobs API routes", () => {
});
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");
const { createJob, updateJob } = await import("@server/repositories/jobs");
const { scoreJobSuitability } = await import("@server/services/scorer");
const { getProfile } = await import("@server/services/profile");
vi.mocked(getProfile).mockResolvedValue({});
vi.mocked(scoreJobSuitability).mockResolvedValue({
@ -618,7 +618,7 @@ describe.sequential("Jobs API routes", () => {
});
it("streams job action progress with done counters", async () => {
const { createJob, updateJob } = await import("../../repositories/jobs");
const { createJob, updateJob } = await import("@server/repositories/jobs");
const discovered = await createJob({
source: "manual",
title: "Discovered Role",
@ -727,7 +727,7 @@ describe.sequential("Jobs API routes", () => {
});
it("applies a job", async () => {
const { createJob } = await import("../../repositories/jobs");
const { createJob } = await import("@server/repositories/jobs");
const job = await createJob({
source: "manual",
title: "Test Role",
@ -746,9 +746,9 @@ describe.sequential("Jobs API routes", () => {
});
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");
const { createJob } = await import("@server/repositories/jobs");
const { scoreJobSuitability } = await import("@server/services/scorer");
const { getProfile } = await import("@server/services/profile");
vi.mocked(getProfile).mockResolvedValue({});
vi.mocked(scoreJobSuitability).mockResolvedValue({
@ -764,7 +764,7 @@ describe.sequential("Jobs API routes", () => {
jobDescription: "Test description",
});
const { updateJob } = await import("../../repositories/jobs");
const { updateJob } = await import("@server/repositories/jobs");
await updateJob(job.id, {
suitabilityScore: 55,
suitabilityReason: "Old fit",
@ -785,7 +785,7 @@ describe.sequential("Jobs API routes", () => {
});
it("deletes jobs below a score threshold (excluding applied)", async () => {
const { createJob, updateJob } = await import("../../repositories/jobs");
const { createJob, updateJob } = await import("@server/repositories/jobs");
// Create jobs with different scores and statuses
const lowScoreJob = await createJob({
@ -883,7 +883,7 @@ describe.sequential("Jobs API routes", () => {
it("checks visa sponsor status for a job", async () => {
const { searchSponsors } = await import(
"../../services/visa-sponsors/index"
"@server/services/visa-sponsors/index"
);
vi.mocked(searchSponsors).mockReturnValue([
{
@ -893,7 +893,7 @@ describe.sequential("Jobs API routes", () => {
},
]);
const { createJob } = await import("../../repositories/jobs");
const { createJob } = await import("@server/repositories/jobs");
const job = await createJob({
source: "manual",
title: "Sponsored Dev",
@ -915,7 +915,7 @@ describe.sequential("Jobs API routes", () => {
let jobId: string;
beforeEach(async () => {
const { createJob } = await import("../../repositories/jobs");
const { createJob } = await import("@server/repositories/jobs");
const job = await createJob({
source: "manual",
title: "Tracking Test",
@ -986,7 +986,7 @@ describe.sequential("Jobs API routes", () => {
});
it("manages application tasks", async () => {
const { db, schema } = await import("../../db/index");
const { db, schema } = await import("@server/db/index");
const { eq } = await import("drizzle-orm");
const { tasks } = schema;

View File

@ -1,7 +1,42 @@
import {
AppError,
type AppErrorCode,
badRequest,
conflict,
notFound,
} from "@infra/errors";
import { fail, ok, okWithMeta } from "@infra/http";
import { logger } from "@infra/logger";
import { sanitizeWebhookPayload } from "@infra/sanitize";
import { setupSse, startSseHeartbeat, writeSseData } from "@infra/sse";
import { isDemoMode, sendDemoBlocked } from "@server/config/demo";
import {
generateFinalPdf,
processJob,
summarizeJob,
} from "@server/pipeline/index";
import * as jobsRepo from "@server/repositories/jobs";
import * as settingsRepo from "@server/repositories/settings";
import {
deleteStageEvent,
getStageEvents,
getTasks,
stageEventMetadataSchema,
transitionStage,
updateStageEvent,
} from "@server/services/applicationTracking";
import {
simulateApplyJob,
simulateGeneratePdf,
simulateProcessJob,
simulateRescoreJob,
simulateSummarizeJob,
} from "@server/services/demo-simulator";
import { getProfile } from "@server/services/profile";
import { scoreJobSuitability } from "@server/services/scorer";
import { getTracerReadiness } from "@server/services/tracer-links";
import * as visaSponsors from "@server/services/visa-sponsors/index";
import { asyncPool } from "@server/utils/async-pool";
import {
APPLICATION_OUTCOMES,
APPLICATION_STAGES,
@ -17,40 +52,6 @@ import {
} from "@shared/types";
import { type Request, type Response, Router } from "express";
import { z } from "zod";
import { isDemoMode, sendDemoBlocked } from "../../config/demo";
import {
AppError,
type AppErrorCode,
badRequest,
conflict,
} from "../../infra/errors";
import {
generateFinalPdf,
processJob,
summarizeJob,
} from "../../pipeline/index";
import * as jobsRepo from "../../repositories/jobs";
import * as settingsRepo from "../../repositories/settings";
import {
deleteStageEvent,
getStageEvents,
getTasks,
stageEventMetadataSchema,
transitionStage,
updateStageEvent,
} from "../../services/applicationTracking";
import {
simulateApplyJob,
simulateGeneratePdf,
simulateProcessJob,
simulateRescoreJob,
simulateSummarizeJob,
} from "../../services/demo-simulator";
import { getProfile } from "../../services/profile";
import { scoreJobSuitability } from "../../services/scorer";
import { getTracerReadiness } from "../../services/tracer-links";
import * as visaSponsors from "../../services/visa-sponsors/index";
import { asyncPool } from "../../utils/async-pool";
export const jobsRouter = Router();
const JOB_ACTION_CONCURRENCY = 4;
@ -884,7 +885,7 @@ jobsRouter.get("/:id", async (req: Request, res: Response) => {
try {
const job = await jobsRepo.getJobById(req.params.id);
if (!job) {
return res.status(404).json({ success: false, error: "Job not found" });
return fail(res, notFound("Job not found"));
}
res.json({ success: true, data: job });
} catch (error) {
@ -996,7 +997,7 @@ jobsRouter.patch("/:id/outcome", async (req: Request, res: Response) => {
});
if (!job) {
return res.status(404).json({ success: false, error: "Job not found" });
return fail(res, notFound("Job not found"));
}
res.json({ success: true, data: job });
@ -1126,7 +1127,7 @@ jobsRouter.post("/:id/summarize", async (req: Request, res: Response) => {
}
const job = await jobsRepo.getJobById(req.params.id);
if (!job) {
return res.status(404).json({ success: false, error: "Job not found" });
return fail(res, notFound("Job not found"));
}
return okWithMeta(res, job, { simulated: true });
}
@ -1153,7 +1154,7 @@ jobsRouter.post("/:id/check-sponsor", async (req: Request, res: Response) => {
const job = await jobsRepo.getJobById(req.params.id);
if (!job) {
return res.status(404).json({ success: false, error: "Job not found" });
return fail(res, notFound("Job not found"));
}
if (!job.employer) {
@ -1203,7 +1204,7 @@ jobsRouter.post("/:id/generate-pdf", async (req: Request, res: Response) => {
}
const job = await jobsRepo.getJobById(req.params.id);
if (!job) {
return res.status(404).json({ success: false, error: "Job not found" });
return fail(res, notFound("Job not found"));
}
return okWithMeta(res, job, { simulated: true });
}
@ -1237,7 +1238,7 @@ jobsRouter.post("/:id/apply", async (req: Request, res: Response) => {
const job = await jobsRepo.getJobById(req.params.id);
if (!job) {
return res.status(404).json({ success: false, error: "Job not found" });
return fail(res, notFound("Job not found"));
}
const appliedAtDate = new Date();
@ -1266,7 +1267,7 @@ jobsRouter.post("/:id/apply", async (req: Request, res: Response) => {
}
if (!updatedJob) {
return res.status(404).json({ success: false, error: "Job not found" });
return fail(res, notFound("Job not found"));
}
res.json({ success: true, data: updatedJob });

View File

@ -46,7 +46,9 @@ describe.sequential("Manual jobs API routes", () => {
});
expect(badRes.status).toBe(400);
const { inferManualJobDetails } = await import("../../services/manualJob");
const { inferManualJobDetails } = await import(
"@server/services/manualJob"
);
vi.mocked(inferManualJobDetails).mockResolvedValue({
job: { title: "Backend Engineer", employer: "Acme" },
warning: null,
@ -63,8 +65,8 @@ describe.sequential("Manual jobs API routes", () => {
});
it("imports manual jobs and generates a fallback URL", async () => {
const { processJob } = await import("../../pipeline/index");
const { scoreJobSuitability } = await import("../../services/scorer");
const { processJob } = await import("@server/pipeline/index");
const { scoreJobSuitability } = await import("@server/services/scorer");
vi.mocked(scoreJobSuitability).mockResolvedValue({
score: 88,
reason: "Strong fit",

View File

@ -1,5 +1,12 @@
import { randomUUID } from "node:crypto";
import { notFound } from "@infra/errors";
import { fail } from "@infra/http";
import { logger } from "@infra/logger";
import { processJob } from "@server/pipeline/index";
import * as jobsRepo from "@server/repositories/jobs";
import { inferManualJobDetails } from "@server/services/manualJob";
import { getProfile } from "@server/services/profile";
import { scoreJobSuitability } from "@server/services/scorer";
import type {
ApiResponse,
ManualJobFetchResponse,
@ -8,11 +15,6 @@ import type {
import { type Request, type Response, Router } from "express";
import { JSDOM } from "jsdom";
import { z } from "zod";
import { processJob } from "../../pipeline/index";
import * as jobsRepo from "../../repositories/jobs";
import { inferManualJobDetails } from "../../services/manualJob";
import { getProfile } from "../../services/profile";
import { scoreJobSuitability } from "../../services/scorer";
export const manualJobsRouter = Router();
@ -252,7 +254,7 @@ manualJobsRouter.post("/import", async (req: Request, res: Response) => {
const processedJob = await jobsRepo.getJobById(createdJob.id);
if (!processedJob) {
return res.status(404).json({ success: false, error: "Job not found" });
return fail(res, notFound("Job not found"));
}
// Score asynchronously so the import returns immediately.

View File

@ -1,5 +1,6 @@
import { okWithMeta } from "@infra/http";
import { logger } from "@infra/logger";
import { isDemoMode } from "@server/config/demo";
import { getSetting } from "@server/repositories/settings";
import { LlmService } from "@server/services/llm/service";
import { RxResumeClient } from "@server/services/rxresume-client";
@ -9,7 +10,6 @@ import {
} from "@server/services/rxresume-v4";
import { resumeDataSchema } from "@shared/rxresume-schema";
import { type Request, type Response, Router } from "express";
import { isDemoMode } from "../../config/demo";
export const onboardingRouter = Router();

View File

@ -32,7 +32,7 @@ describe.sequential("Pipeline API routes", () => {
});
expect(badRun.status).toBe(400);
const { runPipeline } = await import("../../pipeline/index");
const { runPipeline } = await import("@server/pipeline/index");
const runRes = await fetch(`${baseUrl}/api/pipeline/run`, {
method: "POST",
headers: { "Content-Type": "application/json" },
@ -81,7 +81,7 @@ describe.sequential("Pipeline API routes", () => {
});
it("accepts cancellation when pipeline is running", async () => {
const { requestPipelineCancel } = await import("../../pipeline/index");
const { requestPipelineCancel } = await import("@server/pipeline/index");
vi.mocked(requestPipelineCancel).mockReturnValue({
accepted: true,
pipelineRunId: "run-1",

View File

@ -9,23 +9,23 @@ import { fail, ok, okWithMeta } from "@infra/http";
import { logger } from "@infra/logger";
import { runWithRequestContext } from "@infra/request-context";
import { setupSse, startSseHeartbeat, writeSseData } from "@infra/sse";
import { PIPELINE_EXTRACTOR_SOURCE_IDS } from "@shared/extractors";
import type { PipelineStatusResponse } from "@shared/types";
import { type Request, type Response, Router } from "express";
import { z } from "zod";
import { isDemoMode } from "../../config/demo";
import { isDemoMode } from "@server/config/demo";
import {
type ExtractorRegistry,
getExtractorRegistry,
} from "../../extractors/registry";
} from "@server/extractors/registry";
import {
getPipelineStatus,
requestPipelineCancel,
runPipeline,
subscribeToProgress,
} from "../../pipeline/index";
import * as pipelineRepo from "../../repositories/pipeline";
import { simulatePipelineRun } from "../../services/demo-simulator";
} from "@server/pipeline/index";
import * as pipelineRepo from "@server/repositories/pipeline";
import { simulatePipelineRun } from "@server/services/demo-simulator";
import { PIPELINE_EXTRACTOR_SOURCE_IDS } from "@shared/extractors";
import type { PipelineStatusResponse } from "@shared/types";
import { type Request, type Response, Router } from "express";
import { z } from "zod";
export const pipelineRouter = Router();

View File

@ -2,7 +2,7 @@ import type { Server } from "node:http";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { startServer, stopServer } from "./test-utils";
vi.mock("../../services/post-application/providers", () => ({
vi.mock("@server/services/post-application/providers", () => ({
executePostApplicationProviderAction: vi.fn(),
}));
@ -36,7 +36,7 @@ describe.sequential("Post-Application Provider actions API", () => {
it("dispatches provider status action and returns unified success contract", async () => {
const { executePostApplicationProviderAction } = await import(
"../../services/post-application/providers"
"@server/services/post-application/providers"
);
vi.mocked(executePostApplicationProviderAction).mockResolvedValueOnce({
provider: "gmail",
@ -92,7 +92,7 @@ describe.sequential("Post-Application Provider actions API", () => {
it("defaults to account key 'default' when omitted", async () => {
const { executePostApplicationProviderAction } = await import(
"../../services/post-application/providers"
"@server/services/post-application/providers"
);
vi.mocked(executePostApplicationProviderAction).mockResolvedValueOnce({
provider: "gmail",
@ -155,7 +155,7 @@ describe.sequential("Post-Application Provider actions API", () => {
it("maps provider service errors to standardized error responses", async () => {
const { executePostApplicationProviderAction } = await import(
"../../services/post-application/providers"
"@server/services/post-application/providers"
);
const { AppError } = await import("@infra/errors");
vi.mocked(executePostApplicationProviderAction).mockRejectedValueOnce(

View File

@ -2,13 +2,13 @@ import { randomUUID } from "node:crypto";
import { badRequest, serviceUnavailable, upstreamError } from "@infra/errors";
import { asyncRoute, fail, ok } from "@infra/http";
import { logger } from "@infra/logger";
import { executePostApplicationProviderAction } from "@server/services/post-application/providers";
import {
POST_APPLICATION_PROVIDER_ACTIONS,
POST_APPLICATION_PROVIDERS,
} from "@shared/types";
import { type Request, type Response, Router } from "express";
import { z } from "zod";
import { executePostApplicationProviderAction } from "../../services/post-application/providers";
const providerActionParamsSchema = z.object({
provider: z.enum(POST_APPLICATION_PROVIDERS),

View File

@ -29,9 +29,9 @@ describe.sequential("Post-Application Review Workflow API", () => {
message: PostApplicationMessage;
jobId: string;
}> {
const { createJob } = await import("../../repositories/jobs");
const { createJob } = await import("@server/repositories/jobs");
const { upsertPostApplicationMessage } = await import(
"../../repositories/post-application-messages"
"@server/repositories/post-application-messages"
);
const job = await createJob({
@ -93,7 +93,7 @@ describe.sequential("Post-Application Review Workflow API", () => {
it("approves an inbox item and writes stage event", async () => {
const { message, jobId } = await seedPendingMessage();
const { db, schema } = await import("../../db");
const { db, schema } = await import("@server/db");
const res = await fetch(
`${baseUrl}/api/post-application/inbox/${message.id}/approve`,
@ -121,7 +121,7 @@ describe.sequential("Post-Application Review Workflow API", () => {
it("returns conflict on second approve and increments sync-run approval once", async () => {
const { startPostApplicationSyncRun, getPostApplicationSyncRunById } =
await import("../../repositories/post-application-sync-runs");
await import("@server/repositories/post-application-sync-runs");
const run = await startPostApplicationSyncRun({
provider: "gmail",
accountKey: "default",
@ -218,7 +218,7 @@ describe.sequential("Post-Application Review Workflow API", () => {
it("lists messages for a sync run", async () => {
const { startPostApplicationSyncRun } = await import(
"../../repositories/post-application-sync-runs"
"@server/repositories/post-application-sync-runs"
);
const run = await startPostApplicationSyncRun({
provider: "gmail",
@ -243,7 +243,7 @@ describe.sequential("Post-Application Review Workflow API", () => {
const { message, jobId } = await seedPendingMessage({
stageTarget: "rejected",
});
const { db, schema } = await import("../../db");
const { db, schema } = await import("@server/db");
const res = await fetch(
`${baseUrl}/api/post-application/inbox/${message.id}/approve`,
@ -274,7 +274,7 @@ describe.sequential("Post-Application Review Workflow API", () => {
const { message, jobId } = await seedPendingMessage({
stageTarget: "withdrawn",
});
const { db, schema } = await import("../../db");
const { db, schema } = await import("@server/db");
const res = await fetch(
`${baseUrl}/api/post-application/inbox/${message.id}/approve`,

View File

@ -1,12 +1,5 @@
import { badRequest } from "@infra/errors";
import { asyncRoute, fail, ok } from "@infra/http";
import {
APPLICATION_STAGES,
POST_APPLICATION_PROVIDERS,
POST_APPLICATION_ROUTER_STAGE_TARGETS,
} from "@shared/types";
import { type Request, type Response, Router } from "express";
import { z } from "zod";
import {
approvePostApplicationInboxItem,
denyPostApplicationInboxItem,
@ -14,7 +7,14 @@ import {
listPostApplicationReviewRuns,
listPostApplicationRunMessages,
runPostApplicationInboxAction,
} from "../../services/post-application/review";
} from "@server/services/post-application/review";
import {
APPLICATION_STAGES,
POST_APPLICATION_PROVIDERS,
POST_APPLICATION_ROUTER_STAGE_TARGETS,
} from "@shared/types";
import { type Request, type Response, Router } from "express";
import { z } from "zod";
const listQuerySchema = z.object({
provider: z.enum(POST_APPLICATION_PROVIDERS).default("gmail"),

View File

@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { startServer, stopServer } from "./test-utils";
// Mock the rxresume-v4 service
vi.mock("../../services/rxresume-v4", () => ({
vi.mock("@server/services/rxresume-v4", () => ({
getResume: vi.fn(),
listResumes: vi.fn(),
RxResumeCredentialsError: class RxResumeCredentialsError extends Error {
@ -15,13 +15,13 @@ vi.mock("../../services/rxresume-v4", () => ({
}));
// Mock the profile service
vi.mock("../../services/profile", () => ({
vi.mock("@server/services/profile", () => ({
getProfile: vi.fn(),
clearProfileCache: vi.fn(),
}));
// Mock the settings repository
vi.mock("../../repositories/settings", async (importOriginal) => {
vi.mock("@server/repositories/settings", async (importOriginal) => {
const original = (await importOriginal()) as Record<string, unknown>;
return {
...original,
@ -29,12 +29,12 @@ vi.mock("../../repositories/settings", async (importOriginal) => {
};
});
import { getSetting } from "../../repositories/settings";
import { getProfile } from "../../services/profile";
import { getSetting } from "@server/repositories/settings";
import { getProfile } from "@server/services/profile";
import {
getResume,
RxResumeCredentialsError,
} from "../../services/rxresume-v4";
} from "@server/services/rxresume-v4";
describe.sequential("Profile API routes", () => {
let server: Server;

View File

@ -1,13 +1,13 @@
import { type Request, type Response, Router } from "express";
import { isDemoMode } from "../../config/demo";
import { DEMO_PROJECT_CATALOG } from "../../config/demo-defaults";
import { getSetting } from "../../repositories/settings";
import { clearProfileCache, getProfile } from "../../services/profile";
import { extractProjectsFromProfile } from "../../services/resumeProjects";
import { isDemoMode } from "@server/config/demo";
import { DEMO_PROJECT_CATALOG } from "@server/config/demo-defaults";
import { getSetting } from "@server/repositories/settings";
import { clearProfileCache, getProfile } from "@server/services/profile";
import { extractProjectsFromProfile } from "@server/services/resumeProjects";
import {
getResume,
RxResumeCredentialsError,
} from "../../services/rxresume-v4";
} from "@server/services/rxresume-v4";
import { type Request, type Response, Router } from "express";
export const profileRouter = Router();

View File

@ -1,4 +1,5 @@
import { logger } from "@infra/logger";
import { isDemoMode, sendDemoBlocked } from "@server/config/demo";
import { setBackupSettings } from "@server/services/backup/index";
import { extractProjectsFromProfile } from "@server/services/resumeProjects";
import {
@ -10,7 +11,6 @@ import { getEffectiveSettings } from "@server/services/settings";
import { applySettingsUpdates } from "@server/services/settings-update";
import { updateSettingsSchema } from "@shared/settings-schema";
import { type Request, type Response, Router } from "express";
import { isDemoMode, sendDemoBlocked } from "../../config/demo";
export const settingsRouter = Router();

View File

@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { vi } from "vitest";
vi.mock("../../pipeline/index", () => {
vi.mock("@server/pipeline/index", () => {
const progress = {
step: "idle",
message: "Ready",
@ -51,19 +51,19 @@ vi.mock("../../pipeline/index", () => {
};
});
vi.mock("../../services/manualJob", () => ({
vi.mock("@server/services/manualJob", () => ({
inferManualJobDetails: vi.fn(),
}));
vi.mock("../../services/scorer", () => ({
vi.mock("@server/services/scorer", () => ({
scoreJobSuitability: vi.fn(),
}));
vi.mock("../../services/profile", () => ({
vi.mock("@server/services/profile", () => ({
getProfile: vi.fn().mockResolvedValue({}),
}));
vi.mock("../../services/visa-sponsors/index", () => ({
vi.mock("@server/services/visa-sponsors/index", () => ({
getStatus: vi.fn(),
searchSponsors: vi.fn(),
getOrganizationDetails: vi.fn(),
@ -102,13 +102,13 @@ export async function startServer(options?: {
...envOverrides,
};
await import("../../db/migrate");
await import("@server/db/migrate");
const { applyStoredEnvOverrides } = await import(
"../../services/envSettings"
"@server/services/envSettings"
);
const { createApp } = await import("../../app");
const { closeDb } = await import("../../db/index");
const { getPipelineStatus } = await import("../../pipeline/index");
const { closeDb } = await import("@server/db/index");
const { getPipelineStatus } = await import("@server/pipeline/index");
vi.mocked(getPipelineStatus).mockReturnValue({ isRunning: false });
await applyStoredEnvOverrides();

View File

@ -19,7 +19,7 @@ describe.sequential("Tracer links routes", () => {
});
async function seedTracerFixtures() {
const { db, schema } = await import("../../db");
const { db, schema } = await import("@server/db");
const now = new Date().toISOString();
const jobId = "job-tracer-fixture";

View File

@ -1,13 +1,13 @@
import { badRequest, notFound } from "@infra/errors";
import { asyncRoute, fail, ok } from "@infra/http";
import { type Request, type Response, Router } from "express";
import { z } from "zod";
import * as jobsRepo from "../../repositories/jobs";
import * as jobsRepo from "@server/repositories/jobs";
import {
getJobTracerLinksAnalytics,
getTracerAnalytics,
getTracerReadiness,
} from "../../services/tracer-links";
} from "@server/services/tracer-links";
import { type Request, type Response, Router } from "express";
import { z } from "zod";
export const tracerLinksRouter = Router();

View File

@ -18,7 +18,7 @@ describe.sequential("Visa sponsors API routes", () => {
it("returns status and surfaces update errors", async () => {
const { getStatus, downloadLatestCsv } = await import(
"../../services/visa-sponsors/index"
"@server/services/visa-sponsors/index"
);
vi.mocked(getStatus).mockReturnValue({
lastUpdated: null,
@ -46,7 +46,7 @@ describe.sequential("Visa sponsors API routes", () => {
it("validates search payloads and handles missing organizations", async () => {
const { searchSponsors, getOrganizationDetails } = await import(
"../../services/visa-sponsors/index"
"@server/services/visa-sponsors/index"
);
vi.mocked(searchSponsors).mockReturnValue([
{

View File

@ -1,3 +1,6 @@
import { notFound } from "@infra/errors";
import { fail } from "@infra/http";
import * as visaSponsors from "@server/services/visa-sponsors/index";
import type {
ApiResponse,
VisaSponsorSearchResponse,
@ -6,8 +9,6 @@ import type {
import { type Request, type Response, Router } from "express";
import { z } from "zod";
import * as visaSponsors from "../../services/visa-sponsors/index";
export const visaSponsorsRouter = Router();
/**
@ -74,9 +75,7 @@ visaSponsorsRouter.get(
const entries = visaSponsors.getOrganizationDetails(name);
if (entries.length === 0) {
return res
.status(404)
.json({ success: false, error: "Organization not found" });
return fail(res, notFound("Organization not found"));
}
res.json({

View File

@ -2,10 +2,10 @@ import { unauthorized } from "@infra/errors";
import { fail, okWithMeta } from "@infra/http";
import { logger } from "@infra/logger";
import { runWithRequestContext } from "@infra/request-context";
import { isDemoMode } from "@server/config/demo";
import { runPipeline } from "@server/pipeline/index";
import { simulatePipelineRun } from "@server/services/demo-simulator";
import { type Request, type Response, Router } from "express";
import { isDemoMode } from "../../config/demo";
import { runPipeline } from "../../pipeline/index";
import { simulatePipelineRun } from "../../services/demo-simulator";
export const webhookRouter = Router();

View File

@ -3,15 +3,15 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { getProgress, resetProgress } from "../progress";
import { discoverJobsStep } from "./discover-jobs";
vi.mock("../../repositories/settings", () => ({
vi.mock("@server/repositories/settings", () => ({
getAllSettings: vi.fn(),
}));
vi.mock("../../repositories/jobs", () => ({
vi.mock("@server/repositories/jobs", () => ({
getAllJobUrls: vi.fn().mockResolvedValue([]),
}));
vi.mock("../../extractors/registry", () => ({
vi.mock("@server/extractors/registry", () => ({
getExtractorRegistry: vi.fn(),
}));
@ -33,8 +33,8 @@ describe("discoverJobsStep", () => {
});
it("aggregates source errors for enabled sources", async () => {
const settingsRepo = await import("../../repositories/settings");
const registryModule = await import("../../extractors/registry");
const settingsRepo = await import("@server/repositories/settings");
const registryModule = await import("@server/extractors/registry");
const jobspyManifest = {
id: "jobspy",
@ -93,8 +93,8 @@ describe("discoverJobsStep", () => {
});
it("throws when all enabled sources fail", async () => {
const settingsRepo = await import("../../repositories/settings");
const registryModule = await import("../../extractors/registry");
const settingsRepo = await import("@server/repositories/settings");
const registryModule = await import("@server/extractors/registry");
const ukvisaManifest = {
id: "ukvisajobs",
@ -130,8 +130,8 @@ describe("discoverJobsStep", () => {
});
it("throws when all requested sources are incompatible for country", async () => {
const settingsRepo = await import("../../repositories/settings");
const registryModule = await import("../../extractors/registry");
const settingsRepo = await import("@server/repositories/settings");
const registryModule = await import("@server/extractors/registry");
vi.mocked(settingsRepo.getAllSettings).mockResolvedValue({
searchTerms: JSON.stringify(["engineer"]),
@ -157,8 +157,8 @@ describe("discoverJobsStep", () => {
});
it("does not throw when no sources are requested", async () => {
const settingsRepo = await import("../../repositories/settings");
const registryModule = await import("../../extractors/registry");
const settingsRepo = await import("@server/repositories/settings");
const registryModule = await import("@server/extractors/registry");
vi.mocked(settingsRepo.getAllSettings).mockResolvedValue({
searchTerms: JSON.stringify(["engineer"]),
@ -183,8 +183,8 @@ describe("discoverJobsStep", () => {
});
it("drops discovered jobs when employer matches blocked company keywords", async () => {
const settingsRepo = await import("../../repositories/settings");
const registryModule = await import("../../extractors/registry");
const settingsRepo = await import("@server/repositories/settings");
const registryModule = await import("@server/extractors/registry");
const jobspyManifest = {
id: "jobspy",
@ -236,8 +236,8 @@ describe("discoverJobsStep", () => {
});
it("applies shared city filtering for sources without native city filtering", async () => {
const settingsRepo = await import("../../repositories/settings");
const registryModule = await import("../../extractors/registry");
const settingsRepo = await import("@server/repositories/settings");
const registryModule = await import("@server/extractors/registry");
const gradcrackerManifest = {
id: "gradcracker",
@ -313,9 +313,9 @@ describe("discoverJobsStep", () => {
});
it("tracks source completion counters across source transitions", async () => {
const settingsRepo = await import("../../repositories/settings");
const jobsRepo = await import("../../repositories/jobs");
const registryModule = await import("../../extractors/registry");
const settingsRepo = await import("@server/repositories/settings");
const jobsRepo = await import("@server/repositories/jobs");
const registryModule = await import("@server/extractors/registry");
const jobspyManifest = {
id: "jobspy",

View File

@ -1,5 +1,9 @@
import { logger } from "@infra/logger";
import { sanitizeUnknown } from "@infra/sanitize";
import { getExtractorRegistry } from "@server/extractors/registry";
import { getAllJobUrls } from "@server/repositories/jobs";
import * as settingsRepo from "@server/repositories/settings";
import { asyncPool } from "@server/utils/async-pool";
import {
formatCountryLabel,
isSourceAllowedForCountry,
@ -12,10 +16,6 @@ import {
shouldApplyStrictCityFilter,
} from "@shared/search-cities.js";
import type { CreateJobInput, PipelineConfig } from "@shared/types";
import { getExtractorRegistry } from "../../extractors/registry";
import { getAllJobUrls } from "../../repositories/jobs";
import * as settingsRepo from "../../repositories/settings";
import { asyncPool } from "../../utils/async-pool";
import { type CrawlSource, progressHelpers, updateProgress } from "../progress";
const DISCOVERY_CONCURRENCY = 3;

View File

@ -1,6 +1,6 @@
import { logger } from "@infra/logger";
import * as jobsRepo from "@server/repositories/jobs";
import type { CreateJobInput } from "@shared/types";
import * as jobsRepo from "../../repositories/jobs";
import { progressHelpers } from "../progress";
export async function importJobsStep(args: {

View File

@ -1,5 +1,5 @@
import { logger } from "@infra/logger";
import { getProfile } from "../../services/profile";
import { getProfile } from "@server/services/profile";
export async function loadProfileStep(): Promise<Record<string, unknown>> {
logger.info("Loading profile");

View File

@ -1,6 +1,6 @@
import { logger } from "@infra/logger";
import { sanitizeWebhookPayload } from "@infra/sanitize";
import * as settingsRepo from "../../repositories/settings";
import * as settingsRepo from "@server/repositories/settings";
export async function notifyPipelineWebhookStep(
event: "pipeline.completed" | "pipeline.failed",

View File

@ -1,5 +1,5 @@
import { logger } from "@infra/logger";
import { asyncPool } from "../../utils/async-pool";
import { asyncPool } from "@server/utils/async-pool";
import { progressHelpers, updateProgress } from "../progress";
import type { ScoredJob } from "./types";

View File

@ -10,20 +10,20 @@ vi.mock("@infra/logger", () => ({
},
}));
vi.mock("../../repositories/jobs", () => ({
vi.mock("@server/repositories/jobs", () => ({
getUnscoredDiscoveredJobs: vi.fn(),
updateJob: vi.fn(),
}));
vi.mock("../../repositories/settings", () => ({
vi.mock("@server/repositories/settings", () => ({
getSetting: vi.fn(),
}));
vi.mock("../../services/scorer", () => ({
vi.mock("@server/services/scorer", () => ({
scoreJobSuitability: vi.fn(),
}));
vi.mock("../../services/visa-sponsors/index", () => ({
vi.mock("@server/services/visa-sponsors/index", () => ({
searchSponsors: vi.fn(),
calculateSponsorMatchSummary: vi.fn(),
}));
@ -40,10 +40,10 @@ describe("scoreJobsStep auto-skip behavior", () => {
beforeEach(async () => {
vi.clearAllMocks();
const jobsRepo = await import("../../repositories/jobs");
const settingsRepo = await import("../../repositories/settings");
const scorer = await import("../../services/scorer");
const visaSponsors = await import("../../services/visa-sponsors/index");
const jobsRepo = await import("@server/repositories/jobs");
const settingsRepo = await import("@server/repositories/settings");
const scorer = await import("@server/services/scorer");
const visaSponsors = await import("@server/services/visa-sponsors/index");
vi.mocked(jobsRepo.getUnscoredDiscoveredJobs).mockResolvedValue([
createJob({
@ -68,8 +68,8 @@ describe("scoreJobsStep auto-skip behavior", () => {
});
it("auto-skips jobs when score is below threshold", async () => {
const settingsRepo = await import("../../repositories/settings");
const jobsRepo = await import("../../repositories/jobs");
const settingsRepo = await import("@server/repositories/settings");
const jobsRepo = await import("@server/repositories/jobs");
const { logger } = await import("@infra/logger");
vi.mocked(settingsRepo.getSetting).mockResolvedValue("50");
@ -94,9 +94,9 @@ describe("scoreJobsStep auto-skip behavior", () => {
});
it("does not auto-skip jobs when score equals threshold", async () => {
const settingsRepo = await import("../../repositories/settings");
const jobsRepo = await import("../../repositories/jobs");
const scorer = await import("../../services/scorer");
const settingsRepo = await import("@server/repositories/settings");
const jobsRepo = await import("@server/repositories/jobs");
const scorer = await import("@server/services/scorer");
const { logger } = await import("@infra/logger");
vi.mocked(settingsRepo.getSetting).mockResolvedValue("50");
@ -124,8 +124,8 @@ describe("scoreJobsStep auto-skip behavior", () => {
});
it("does not auto-skip when threshold setting is null", async () => {
const settingsRepo = await import("../../repositories/settings");
const jobsRepo = await import("../../repositories/jobs");
const settingsRepo = await import("@server/repositories/settings");
const jobsRepo = await import("@server/repositories/jobs");
vi.mocked(settingsRepo.getSetting).mockResolvedValue(null);
@ -138,8 +138,8 @@ describe("scoreJobsStep auto-skip behavior", () => {
});
it("does not auto-skip when threshold setting is NaN", async () => {
const settingsRepo = await import("../../repositories/settings");
const jobsRepo = await import("../../repositories/jobs");
const settingsRepo = await import("@server/repositories/settings");
const jobsRepo = await import("@server/repositories/jobs");
vi.mocked(settingsRepo.getSetting).mockResolvedValue("not-a-number");
@ -152,8 +152,8 @@ describe("scoreJobsStep auto-skip behavior", () => {
});
it("never auto-skips applied jobs even when score is below threshold", async () => {
const settingsRepo = await import("../../repositories/settings");
const jobsRepo = await import("../../repositories/jobs");
const settingsRepo = await import("@server/repositories/settings");
const jobsRepo = await import("@server/repositories/jobs");
const { logger } = await import("@infra/logger");
vi.mocked(settingsRepo.getSetting).mockResolvedValue("50");
@ -185,8 +185,8 @@ describe("scoreJobsStep auto-skip behavior", () => {
});
it("scores multiple jobs and reports completion progress", async () => {
const jobsRepo = await import("../../repositories/jobs");
const scorer = await import("../../services/scorer");
const jobsRepo = await import("@server/repositories/jobs");
const scorer = await import("@server/services/scorer");
const { progressHelpers } = await import("../progress");
vi.mocked(jobsRepo.getUnscoredDiscoveredJobs).mockResolvedValue([
@ -217,8 +217,8 @@ describe("scoreJobsStep auto-skip behavior", () => {
});
it("stops before processing when cancellation is requested", async () => {
const jobsRepo = await import("../../repositories/jobs");
const scorer = await import("../../services/scorer");
const jobsRepo = await import("@server/repositories/jobs");
const scorer = await import("@server/services/scorer");
vi.mocked(jobsRepo.getUnscoredDiscoveredJobs).mockResolvedValue([
createJob({

View File

@ -1,10 +1,10 @@
import { logger } from "@infra/logger";
import * as jobsRepo from "@server/repositories/jobs";
import * as settingsRepo from "@server/repositories/settings";
import { scoreJobSuitability } from "@server/services/scorer";
import * as visaSponsors from "@server/services/visa-sponsors/index";
import { asyncPool } from "@server/utils/async-pool";
import type { Job } from "@shared/types";
import * as jobsRepo from "../../repositories/jobs";
import * as settingsRepo from "../../repositories/settings";
import { scoreJobSuitability } from "../../services/scorer";
import * as visaSponsors from "../../services/visa-sponsors/index";
import { asyncPool } from "../../utils/async-pool";
import { progressHelpers, updateProgress } from "../progress";
import type { ScoredJob } from "./types";

View File

@ -6,11 +6,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import * as backup from "./index";
// Mock the dataDir module
vi.mock("../../config/dataDir", () => ({
vi.mock("@server/config/dataDir", () => ({
getDataDir: vi.fn(),
}));
import { getDataDir } from "../../config/dataDir";
import { getDataDir } from "@server/config/dataDir";
describe("Backup Service", () => {
let tempDir: string;

View File

@ -8,10 +8,10 @@
import fs from "node:fs";
import type { FileHandle } from "node:fs/promises";
import path from "node:path";
import { getDataDir } from "@server/config/dataDir";
import { createScheduler } from "@server/utils/scheduler";
import type { BackupInfo } from "@shared/types";
import Database from "better-sqlite3";
import { getDataDir } from "../../config/dataDir";
import { createScheduler } from "../../utils/scheduler";
const DB_FILENAME = "jobs.db";
const AUTO_BACKUP_PREFIX = "jobs_";

View File

@ -1,6 +1,6 @@
import type { AppError } from "@infra/errors";
import { createJob } from "@shared/testing/factories";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { AppError } from "../infra/errors";
import { buildJobChatPromptContext } from "./ghostwriter-context";
vi.mock("../repositories/jobs", () => ({

View File

@ -1,7 +1,7 @@
import { badRequest, notFound } from "@infra/errors";
import { logger } from "@infra/logger";
import { sanitizeUnknown } from "@infra/sanitize";
import type { Job, ResumeProfile } from "@shared/types";
import { badRequest, notFound } from "../infra/errors";
import * as jobsRepo from "../repositories/jobs";
import { getProfile } from "./profile";
import { getEffectiveSettings } from "./settings";

View File

@ -1,13 +1,13 @@
import { logger } from "@infra/logger";
import { getRequestId } from "@infra/request-context";
import type { JobChatMessage, JobChatRun } from "@shared/types";
import {
badRequest,
conflict,
notFound,
requestTimeout,
upstreamError,
} from "../infra/errors";
} from "@infra/errors";
import { logger } from "@infra/logger";
import { getRequestId } from "@infra/request-context";
import type { JobChatMessage, JobChatRun } from "@shared/types";
import * as jobChatRepo from "../repositories/ghostwriter";
import * as settingsRepo from "../repositories/settings";
import { buildJobChatPromptContext } from "./ghostwriter-context";

View File

@ -1,12 +1,14 @@
import { logger } from "@infra/logger";
export function parseJsonContent<T>(content: string, jobId?: string): T {
let candidate = content.trim();
candidate = candidate
export function stripMarkdownCodeFences(content: string): string {
return content
.replace(/```(?:json|JSON)?\s*/g, "")
.replace(/```/g, "")
.trim();
}
export function parseJsonContent<T>(content: string, jobId?: string): T {
let candidate = stripMarkdownCodeFences(content);
const firstBrace = candidate.indexOf("{");
const lastBrace = candidate.lastIndexOf("}");

View File

@ -11,6 +11,7 @@ import type {
PostApplicationRouterStageTarget,
} from "@shared/types";
import { POST_APPLICATION_ROUTER_STAGE_TARGETS } from "@shared/types";
import { normalizeWhitespace } from "@shared/utils/string";
export const ROUTER_EMAIL_CHAR_LIMIT = 12_000;
@ -91,7 +92,7 @@ export function minifyActiveJobs(jobs: Job[]): Array<{
}
function sanitizeJobPromptValue(value: string): string {
return value.replace(/\s+/g, " ").trim();
return normalizeWhitespace(value);
}
export function buildIndexedActiveJobs(

View File

@ -1,4 +1,5 @@
import { requestTimeout } from "@infra/errors";
import { normalizeWhitespace } from "@shared/utils/string";
import { convert } from "html-to-text";
export const GMAIL_HTTP_TIMEOUT_MS = 15_000;
@ -313,7 +314,7 @@ function cleanEmailHtmlForLlm(htmlContent: string): string {
}
function normalizeChunkForDedup(value: string): string {
return value.replace(/\s+/g, " ").trim().toLowerCase();
return normalizeWhitespace(value).toLowerCase();
}
function decodeBase64Url(value: string): string {

View File

@ -3,6 +3,7 @@ import type {
ResumeProjectCatalogItem,
ResumeProjectsSettings,
} from "@shared/types";
import { stripHtmlTags } from "@shared/utils/string";
type ResumeProjectSelectionItem = ResumeProjectCatalogItem & {
summaryText: string;
@ -156,8 +157,7 @@ export function resolveResumeProjectsSettings(args: {
}
export function stripHtml(input: string): string {
const withoutTags = input.replace(/<[^>]*>/g, " ");
return withoutTags.replace(/\s+/g, " ").trim();
return stripHtmlTags(input);
}
function uniqueStrings(values: string[]): string[] {

View File

@ -5,6 +5,7 @@
// - The v5 client should be a drop-in replacement in the future.
import type { ResumeData } from "@shared/rxresume-schema";
import { normalizeWhitespace } from "@shared/utils/string";
type AnyObj = Record<string, unknown>;
const MAX_ERROR_SNIPPET = 300;
@ -457,6 +458,6 @@ export class RxResumeClient {
function sanitizeResponseSnippet(text: string): string {
if (!text) return "";
const compact = text.replace(/\s+/g, " ").trim();
const compact = normalizeWhitespace(text);
return compact.slice(0, MAX_ERROR_SNIPPET);
}

View File

@ -7,6 +7,7 @@ import type { Job } from "@shared/types";
import { getSetting } from "../repositories/settings";
import { LlmService } from "./llm/service";
import type { JsonSchemaDefinition } from "./llm/types";
import { stripMarkdownCodeFences } from "./llm/utils/json";
import { getEffectiveSettings } from "./settings";
interface SuitabilityResult {
@ -162,10 +163,7 @@ export function parseJsonFromContent(
let candidate = content.trim();
// Step 1: Remove markdown code fences (with or without language specifier)
candidate = candidate
.replace(/```(?:json|JSON)?\s*/g, "")
.replace(/```/g, "")
.trim();
candidate = stripMarkdownCodeFences(candidate);
// Step 2: Try to extract JSON object if there's surrounding text
const jsonMatch = candidate.match(/\{[\s\S]*\}/);

View File

@ -6,8 +6,14 @@
import fs from "node:fs";
import path from "node:path";
import { getDataDir } from "../../config/dataDir";
import { createScheduler } from "../../utils/scheduler";
import { getDataDir } from "@server/config/dataDir";
import { createScheduler } from "@server/utils/scheduler";
import type {
VisaSponsor,
VisaSponsorSearchResult,
VisaSponsorStatusResponse,
} from "@shared/types";
import { normalizeWhitespace } from "@shared/utils/string";
const DATA_DIR = path.join(getDataDir(), "visa-sponsors");
@ -16,28 +22,8 @@ if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true });
}
export interface VisaSponsor {
organisationName: string;
townCity: string;
county: string;
typeRating: string;
route: string;
}
export interface VisaSponsorSearchResult {
sponsor: VisaSponsor;
score: number;
matchedName: string;
}
export interface VisaSponsorStatus {
lastUpdated: string | null;
csvPath: string | null;
totalSponsors: number;
isUpdating: boolean;
nextScheduledUpdate: string | null;
error: string | null;
}
export type { VisaSponsor, VisaSponsorSearchResult };
export type VisaSponsorStatus = VisaSponsorStatusResponse;
// Common company suffixes to strip during comparison
const COMPANY_SUFFIXES = [
@ -86,7 +72,7 @@ export function normalizeCompanyName(name: string): string {
}
// Collapse whitespace
normalized = normalized.replace(/\s+/g, " ").trim();
normalized = normalizeWhitespace(normalized);
return normalized;
}

View File

@ -0,0 +1,7 @@
export function normalizeWhitespace(value: string): string {
return value.replace(/\s+/g, " ").trim();
}
export function stripHtmlTags(value: string): string {
return normalizeWhitespace(value.replace(/<[^>]*>/g, " "));
}