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:
parent
16acdf2b5e
commit
3da5ea35b4
60
biome.json
60
biome.json
@ -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"],
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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(),
|
||||
}));
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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" />,
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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(),
|
||||
}));
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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, "_");
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 });
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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`,
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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([
|
||||
{
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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_";
|
||||
|
||||
@ -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", () => ({
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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("}");
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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[] {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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]*\}/);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
7
shared/src/utils/string.ts
Normal file
7
shared/src/utils/string.ts
Normal 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, " "));
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user