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
|
"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": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"includes": ["**/*.test.ts", "**/*.test.tsx", "**/test-utils.ts"],
|
"includes": ["**/*.test.ts", "**/*.test.tsx", "**/test-utils.ts"],
|
||||||
|
|||||||
@ -63,13 +63,6 @@ export interface AdzunaResult {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldApplyStrictLocationFilter(
|
|
||||||
location: string,
|
|
||||||
countryKey: string,
|
|
||||||
): boolean {
|
|
||||||
return shouldApplyStrictCityFilter(location, countryKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveTsxCliPath(): string | null {
|
function resolveTsxCliPath(): string | null {
|
||||||
try {
|
try {
|
||||||
return require.resolve("tsx/dist/cli.mjs");
|
return require.resolve("tsx/dist/cli.mjs");
|
||||||
@ -214,8 +207,7 @@ export async function runAdzuna(
|
|||||||
for (let runIndex = 0; runIndex < runLocations.length; runIndex += 1) {
|
for (let runIndex = 0; runIndex < runLocations.length; runIndex += 1) {
|
||||||
const location = runLocations[runIndex];
|
const location = runLocations[runIndex];
|
||||||
const strictLocationFilter =
|
const strictLocationFilter =
|
||||||
location !== null &&
|
location !== null && shouldApplyStrictCityFilter(location, countryKey);
|
||||||
shouldApplyStrictLocationFilter(location, countryKey);
|
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const extractorEnv = {
|
const extractorEnv = {
|
||||||
|
|||||||
@ -1,15 +1,13 @@
|
|||||||
|
import { shouldApplyStrictCityFilter } from "@shared/search-cities.js";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { shouldApplyStrictLocationFilter } from "../src/run";
|
|
||||||
|
|
||||||
describe("adzuna location query strictness", () => {
|
describe("adzuna location query strictness", () => {
|
||||||
it("enables strict filtering when city differs from country", () => {
|
it("enables strict filtering when city differs from country", () => {
|
||||||
expect(shouldApplyStrictLocationFilter("Leeds", "united kingdom")).toBe(
|
expect(shouldApplyStrictCityFilter("Leeds", "united kingdom")).toBe(true);
|
||||||
true,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("disables strict filtering when location is country-level", () => {
|
it("disables strict filtering when location is country-level", () => {
|
||||||
expect(shouldApplyStrictLocationFilter("UK", "united kingdom")).toBe(false);
|
expect(shouldApplyStrictCityFilter("UK", "united kingdom")).toBe(false);
|
||||||
expect(shouldApplyStrictLocationFilter("United States", "us")).toBe(false);
|
expect(shouldApplyStrictCityFilter("United States", "us")).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -65,13 +65,6 @@ export interface HiringCafeResult {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldApplyStrictLocationFilter(
|
|
||||||
location: string,
|
|
||||||
countryKey: string,
|
|
||||||
): boolean {
|
|
||||||
return shouldApplyStrictCityFilter(location, countryKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveTsxCliPath(): string | null {
|
function resolveTsxCliPath(): string | null {
|
||||||
try {
|
try {
|
||||||
return require.resolve("tsx/dist/cli.mjs");
|
return require.resolve("tsx/dist/cli.mjs");
|
||||||
@ -213,8 +206,7 @@ export async function runHiringCafe(
|
|||||||
for (let runIndex = 0; runIndex < runLocations.length; runIndex += 1) {
|
for (let runIndex = 0; runIndex < runLocations.length; runIndex += 1) {
|
||||||
const location = runLocations[runIndex];
|
const location = runLocations[runIndex];
|
||||||
const strictLocationFilter =
|
const strictLocationFilter =
|
||||||
location !== null &&
|
location !== null && shouldApplyStrictCityFilter(location, countryKey);
|
||||||
shouldApplyStrictLocationFilter(location, countryKey);
|
|
||||||
|
|
||||||
await clearStorageDataset();
|
await clearStorageDataset();
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,13 @@
|
|||||||
|
import { shouldApplyStrictCityFilter } from "@shared/search-cities.js";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { shouldApplyStrictLocationFilter } from "../src/run";
|
|
||||||
|
|
||||||
describe("hiringcafe location query strictness", () => {
|
describe("hiringcafe location query strictness", () => {
|
||||||
it("enables strict filtering when city differs from country", () => {
|
it("enables strict filtering when city differs from country", () => {
|
||||||
expect(shouldApplyStrictLocationFilter("Leeds", "united kingdom")).toBe(
|
expect(shouldApplyStrictCityFilter("Leeds", "united kingdom")).toBe(true);
|
||||||
true,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("disables strict filtering when location is country-level", () => {
|
it("disables strict filtering when location is country-level", () => {
|
||||||
expect(shouldApplyStrictLocationFilter("UK", "united kingdom")).toBe(false);
|
expect(shouldApplyStrictCityFilter("UK", "united kingdom")).toBe(false);
|
||||||
expect(shouldApplyStrictLocationFilter("United States", "us")).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 { createJob } from "@shared/testing/factories.js";
|
||||||
import type { Job } from "@shared/types.js";
|
import type { Job } from "@shared/types.js";
|
||||||
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||||
@ -5,8 +7,6 @@ import type React from "react";
|
|||||||
import { MemoryRouter } from "react-router-dom";
|
import { MemoryRouter } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import * as api from "../../api";
|
|
||||||
import { renderWithQueryClient } from "../../test/renderWithQueryClient";
|
|
||||||
import { DiscoveredPanel } from "./DiscoveredPanel";
|
import { DiscoveredPanel } from "./DiscoveredPanel";
|
||||||
|
|
||||||
const render = (ui: Parameters<typeof renderWithQueryClient>[0]) =>
|
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 }),
|
useSettings: () => ({ showSponsorInfo: false }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../api", () => ({
|
vi.mock("@client/api", () => ({
|
||||||
rescoreJob: vi.fn(),
|
rescoreJob: vi.fn(),
|
||||||
skipJob: vi.fn(),
|
skipJob: vi.fn(),
|
||||||
processJob: 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 { Job } from "@shared/types.js";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
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 { JobDetailsEditDrawer } from "../JobDetailsEditDrawer";
|
||||||
import { DecideMode } from "./DecideMode";
|
import { DecideMode } from "./DecideMode";
|
||||||
import { EmptyState } from "./EmptyState";
|
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 { createJob as createBaseJob } from "@shared/testing/factories.js";
|
||||||
import type { Job } from "@shared/types.js";
|
import type { Job } from "@shared/types.js";
|
||||||
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
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";
|
import { TailorMode } from "./TailorMode";
|
||||||
|
|
||||||
const render = (ui: Parameters<typeof renderWithQueryClient>[0]) =>
|
const render = (ui: Parameters<typeof renderWithQueryClient>[0]) =>
|
||||||
renderWithQueryClient(ui);
|
renderWithQueryClient(ui);
|
||||||
|
|
||||||
vi.mock("../../api", () => ({
|
vi.mock("@client/api", () => ({
|
||||||
getResumeProjectsCatalog: vi.fn().mockResolvedValue([]),
|
getResumeProjectsCatalog: vi.fn().mockResolvedValue([]),
|
||||||
updateJob: vi.fn(),
|
updateJob: vi.fn(),
|
||||||
summarizeJob: vi.fn(),
|
summarizeJob: vi.fn(),
|
||||||
getTracerReadiness: vi.fn(),
|
getTracerReadiness: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../hooks/useProfile", () => ({
|
vi.mock("@client/hooks/useProfile", () => ({
|
||||||
useProfile: vi.fn(),
|
useProfile: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
|
import * as api from "@client/api";
|
||||||
import type { Job, JobChatMessage, JobChatStreamEvent } from "@shared/types";
|
import type { Job, JobChatMessage, JobChatStreamEvent } from "@shared/types";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import * as api from "../../api";
|
|
||||||
import { Composer } from "./Composer";
|
import { Composer } from "./Composer";
|
||||||
import { MessageList } from "./MessageList";
|
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 type { Job } from "@shared/types.js";
|
||||||
import { ArrowLeft, Check, FileText, Loader2, Sparkles } from "lucide-react";
|
import { ArrowLeft, Check, FileText, Loader2, Sparkles } from "lucide-react";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
@ -6,9 +9,6 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import * as api from "../../api";
|
|
||||||
import { useProfile } from "../../hooks/useProfile";
|
|
||||||
import { useTracerReadiness } from "../../hooks/useTracerReadiness";
|
|
||||||
import {
|
import {
|
||||||
fromEditableSkillGroups,
|
fromEditableSkillGroups,
|
||||||
getOriginalHeadline,
|
getOriginalHeadline,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
|
import * as api from "@client/api";
|
||||||
import type { Job, ResumeProjectCatalogItem } from "@shared/types.js";
|
import type { Job, ResumeProjectCatalogItem } from "@shared/types.js";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import * as api from "../../api";
|
|
||||||
import {
|
import {
|
||||||
createTailoredSkillDraftId,
|
createTailoredSkillDraftId,
|
||||||
type EditableSkillGroup,
|
type EditableSkillGroup,
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { CollapsibleSection } from "@client/components/discovered-panel/CollapsibleSection";
|
||||||
import {
|
import {
|
||||||
type ApplicationStage,
|
type ApplicationStage,
|
||||||
STAGE_LABELS,
|
STAGE_LABELS,
|
||||||
@ -18,7 +19,6 @@ import {
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { cn, formatTimestamp, formatTimestampWithTime } from "@/lib/utils";
|
import { cn, formatTimestamp, formatTimestampWithTime } from "@/lib/utils";
|
||||||
import { CollapsibleSection } from "../../components/discovered-panel/CollapsibleSection";
|
|
||||||
|
|
||||||
const stageIcons: Record<ApplicationStage, React.ReactNode> = {
|
const stageIcons: Record<ApplicationStage, React.ReactNode> = {
|
||||||
applied: <CheckCircle2 className="h-4 w-4" />,
|
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 { createJob } from "@shared/testing/factories.js";
|
||||||
import type { Job } from "@shared/types.js";
|
import type { Job } from "@shared/types.js";
|
||||||
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
|
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import * as api from "../../api";
|
|
||||||
import { renderWithQueryClient } from "../../test/renderWithQueryClient";
|
|
||||||
import { JobDetailPanel } from "./JobDetailPanel";
|
import { JobDetailPanel } from "./JobDetailPanel";
|
||||||
|
|
||||||
const render = (ui: Parameters<typeof renderWithQueryClient>[0]) =>
|
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 }) => (
|
DiscoveredPanel: ({ job }: { job: Job | null }) => (
|
||||||
<div data-testid="discovered-panel">{job?.id ?? "no-job"}</div>
|
<div data-testid="discovered-panel">{job?.id ?? "no-job"}</div>
|
||||||
),
|
),
|
||||||
@ -51,7 +51,7 @@ vi.mock("../../components", () => ({
|
|||||||
TailoredSummary: () => <div data-testid="tailored-summary" />,
|
TailoredSummary: () => <div data-testid="tailored-summary" />,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../components/ReadyPanel", () => ({
|
vi.mock("@client/components/ReadyPanel", () => ({
|
||||||
ReadyPanel: ({ onEditDescription }: { onEditDescription?: () => void }) => (
|
ReadyPanel: ({ onEditDescription }: { onEditDescription?: () => void }) => (
|
||||||
<div>
|
<div>
|
||||||
<div data-testid="ready-panel" />
|
<div data-testid="ready-panel" />
|
||||||
@ -62,7 +62,7 @@ vi.mock("../../components/ReadyPanel", () => ({
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../components/TailoringEditor", () => ({
|
vi.mock("@client/components/TailoringEditor", () => ({
|
||||||
TailoringEditor: ({
|
TailoringEditor: ({
|
||||||
onDirtyChange,
|
onDirtyChange,
|
||||||
}: {
|
}: {
|
||||||
@ -79,7 +79,7 @@ vi.mock("../../components/TailoringEditor", () => ({
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../components/JobDetailsEditDrawer", () => ({
|
vi.mock("@client/components/JobDetailsEditDrawer", () => ({
|
||||||
JobDetailsEditDrawer: ({
|
JobDetailsEditDrawer: ({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
@ -116,7 +116,7 @@ vi.mock("@/lib/utils", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock("../../api", () => ({
|
vi.mock("@client/api", () => ({
|
||||||
updateJob: vi.fn(),
|
updateJob: vi.fn(),
|
||||||
processJob: vi.fn(),
|
processJob: vi.fn(),
|
||||||
generateJobPdf: 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 type { Job, JobListItem } from "@shared/types.js";
|
||||||
import {
|
import {
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
@ -32,21 +47,6 @@ import {
|
|||||||
safeFilenamePart,
|
safeFilenamePart,
|
||||||
stripHtml,
|
stripHtml,
|
||||||
} from "@/lib/utils";
|
} 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";
|
import type { FilterTab } from "./constants";
|
||||||
|
|
||||||
interface JobDetailPanelProps {
|
interface JobDetailPanelProps {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
|
import { PipelineProgress } from "@client/components";
|
||||||
import type { JobStatus } from "@shared/types.js";
|
import type { JobStatus } from "@shared/types.js";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { PipelineProgress } from "../../components";
|
|
||||||
|
|
||||||
interface OrchestratorSummaryProps {
|
interface OrchestratorSummaryProps {
|
||||||
stats: Record<JobStatus, number>;
|
stats: Record<JobStatus, number>;
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
|
import * as api from "@client/api";
|
||||||
import { createJob } from "@shared/testing/factories.js";
|
import { createJob } from "@shared/testing/factories.js";
|
||||||
import type { JobActionResponse, JobActionStreamEvent } from "@shared/types.js";
|
import type { JobActionResponse, JobActionStreamEvent } from "@shared/types.js";
|
||||||
import { act, renderHook, waitFor } from "@testing-library/react";
|
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import * as api from "../../api";
|
|
||||||
import { useJobSelectionActions } from "./useJobSelectionActions";
|
import { useJobSelectionActions } from "./useJobSelectionActions";
|
||||||
|
|
||||||
vi.mock("../../api", () => ({
|
vi.mock("@client/api", () => ({
|
||||||
streamJobAction: vi.fn(),
|
streamJobAction: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import * as api from "@client/api";
|
||||||
import type {
|
import type {
|
||||||
JobAction,
|
JobAction,
|
||||||
JobActionResponse,
|
JobActionResponse,
|
||||||
@ -5,7 +6,6 @@ import type {
|
|||||||
} from "@shared/types.js";
|
} from "@shared/types.js";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import * as api from "../../api";
|
|
||||||
import type { FilterTab } from "./constants";
|
import type { FilterTab } from "./constants";
|
||||||
import { JobActionProgressToast } from "./JobActionProgressToast";
|
import { JobActionProgressToast } from "./JobActionProgressToast";
|
||||||
import {
|
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 { act, waitFor } from "@testing-library/react";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import * as api from "../../api";
|
|
||||||
import { renderHookWithQueryClient } from "../../test/renderWithQueryClient";
|
|
||||||
import { useOrchestratorData } from "./useOrchestratorData";
|
import { useOrchestratorData } from "./useOrchestratorData";
|
||||||
|
|
||||||
const renderHook = (callback: () => ReturnType<typeof useOrchestratorData>) =>
|
const renderHook = (callback: () => ReturnType<typeof useOrchestratorData>) =>
|
||||||
renderHookWithQueryClient(callback);
|
renderHookWithQueryClient(callback);
|
||||||
|
|
||||||
vi.mock("../../api", () => ({
|
vi.mock("@client/api", () => ({
|
||||||
getJobs: vi.fn(),
|
getJobs: vi.fn(),
|
||||||
getJobsRevision: vi.fn(),
|
getJobsRevision: vi.fn(),
|
||||||
getJob: 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 type { Job, JobListItem, JobStatus } from "@shared/types";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { queryKeys } from "@/client/lib/queryKeys";
|
import { queryKeys } from "@/client/lib/queryKeys";
|
||||||
import * as api from "../../api";
|
|
||||||
import { subscribeToEventSource } from "../../lib/sse";
|
|
||||||
|
|
||||||
const initialStats: Record<JobStatus, number> = {
|
const initialStats: Record<JobStatus, number> = {
|
||||||
discovered: 0,
|
discovered: 0,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import {
|
|||||||
sourceLabel as getExtractorSourceLabel,
|
sourceLabel as getExtractorSourceLabel,
|
||||||
} from "@shared/extractors";
|
} from "@shared/extractors";
|
||||||
import type { Job } from "@shared/types";
|
import type { Job } from "@shared/types";
|
||||||
|
import { stripHtmlTags } from "@shared/utils/string";
|
||||||
import { type ClassValue, clsx } from "clsx";
|
import { type ClassValue, clsx } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
@ -101,11 +102,7 @@ export async function copyTextToClipboard(text: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Text Processing ---
|
// --- Text Processing ---
|
||||||
export const stripHtml = (value: string) =>
|
export const stripHtml = (value: string) => stripHtmlTags(value);
|
||||||
value
|
|
||||||
.replace(/<[^>]*>/g, " ")
|
|
||||||
.replace(/\s+/g, " ")
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
export const safeFilenamePart = (value: string) => {
|
export const safeFilenamePart = (value: string) => {
|
||||||
const cleaned = value.replace(/[^a-z0-9]/gi, "_");
|
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 { logger } from "@infra/logger";
|
||||||
|
import { isDemoMode, sendDemoBlocked } from "@server/config/demo";
|
||||||
import {
|
import {
|
||||||
createBackup,
|
createBackup,
|
||||||
deleteBackup,
|
deleteBackup,
|
||||||
@ -6,7 +9,6 @@ import {
|
|||||||
listBackups,
|
listBackups,
|
||||||
} from "@server/services/backup/index";
|
} from "@server/services/backup/index";
|
||||||
import { type Request, type Response, Router } from "express";
|
import { type Request, type Response, Router } from "express";
|
||||||
import { isDemoMode, sendDemoBlocked } from "../../config/demo";
|
|
||||||
|
|
||||||
export const backupRouter = Router();
|
export const backupRouter = Router();
|
||||||
|
|
||||||
@ -104,7 +106,7 @@ backupRouter.delete("/:filename", async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (message.includes("not found")) {
|
if (message.includes("not found")) {
|
||||||
res.status(404).json({ success: false, error: message });
|
return fail(res, notFound(message));
|
||||||
} else if (message.includes("Invalid")) {
|
} else if (message.includes("Invalid")) {
|
||||||
res.status(400).json({ success: false, error: message });
|
res.status(400).json({ success: false, error: message });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -17,7 +17,7 @@ describe.sequential("Database API routes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("clears jobs and pipeline runs", async () => {
|
it("clears jobs and pipeline runs", async () => {
|
||||||
const { createJob } = await import("../../repositories/jobs");
|
const { createJob } = await import("@server/repositories/jobs");
|
||||||
await createJob({
|
await createJob({
|
||||||
source: "manual",
|
source: "manual",
|
||||||
title: "Cleanup Role",
|
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 { type Request, type Response, Router } from "express";
|
||||||
import { isDemoMode, sendDemoBlocked } from "../../config/demo";
|
|
||||||
import { clearDatabase } from "../../db/clear";
|
|
||||||
|
|
||||||
export const databaseRouter = Router();
|
export const databaseRouter = Router();
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ok } from "@infra/http";
|
import { ok } from "@infra/http";
|
||||||
|
import { getDemoInfo } from "@server/config/demo";
|
||||||
import { type Request, type Response, Router } from "express";
|
import { type Request, type Response, Router } from "express";
|
||||||
import { getDemoInfo } from "../../config/demo";
|
|
||||||
|
|
||||||
export const demoRouter = Router();
|
export const demoRouter = Router();
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type { Server } from "node:http";
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { startServer, stopServer } from "./test-utils";
|
import { startServer, stopServer } from "./test-utils";
|
||||||
|
|
||||||
vi.mock("../../services/ghostwriter", () => ({
|
vi.mock("@server/services/ghostwriter", () => ({
|
||||||
listThreads: vi.fn(async () => [
|
listThreads: vi.fn(async () => [
|
||||||
{
|
{
|
||||||
id: "thread-1",
|
id: "thread-1",
|
||||||
|
|||||||
@ -2,9 +2,9 @@ import { asyncRoute, fail, ok } from "@infra/http";
|
|||||||
import { runWithRequestContext } from "@infra/request-context";
|
import { runWithRequestContext } from "@infra/request-context";
|
||||||
import { setupSse, writeSseData } from "@infra/sse";
|
import { setupSse, writeSseData } from "@infra/sse";
|
||||||
import { badRequest, toAppError } from "@server/infra/errors";
|
import { badRequest, toAppError } from "@server/infra/errors";
|
||||||
|
import * as ghostwriterService from "@server/services/ghostwriter";
|
||||||
import { type Request, Router } from "express";
|
import { type Request, Router } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import * as ghostwriterService from "../../services/ghostwriter";
|
|
||||||
|
|
||||||
export const ghostwriterRouter = Router({ mergeParams: true });
|
export const ghostwriterRouter = Router({ mergeParams: true });
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@ describe.sequential("Jobs API routes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("lists jobs and supports status filtering", async () => {
|
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({
|
const job = await createJob({
|
||||||
source: "manual",
|
source: "manual",
|
||||||
title: "Test Role",
|
title: "Test Role",
|
||||||
@ -40,7 +40,7 @@ describe.sequential("Jobs API routes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("supports lightweight and full jobs list views", async () => {
|
it("supports lightweight and full jobs list views", async () => {
|
||||||
const { createJob } = await import("../../repositories/jobs");
|
const { createJob } = await import("@server/repositories/jobs");
|
||||||
await createJob({
|
await createJob({
|
||||||
source: "manual",
|
source: "manual",
|
||||||
title: "List View Role",
|
title: "List View Role",
|
||||||
@ -76,7 +76,7 @@ describe.sequential("Jobs API routes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns jobs revision and supports status filtering", async () => {
|
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({
|
const readyJob = await createJob({
|
||||||
source: "manual",
|
source: "manual",
|
||||||
title: "Ready Role",
|
title: "Ready Role",
|
||||||
@ -133,7 +133,7 @@ describe.sequential("Jobs API routes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("updates core job detail fields", async () => {
|
it("updates core job detail fields", async () => {
|
||||||
const { createJob } = await import("../../repositories/jobs");
|
const { createJob } = await import("@server/repositories/jobs");
|
||||||
const job = await createJob({
|
const job = await createJob({
|
||||||
source: "manual",
|
source: "manual",
|
||||||
title: "Original Title",
|
title: "Original Title",
|
||||||
@ -176,7 +176,7 @@ describe.sequential("Jobs API routes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("blocks enabling tracer links when readiness check fails", async () => {
|
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({
|
const job = await createJob({
|
||||||
source: "manual",
|
source: "manual",
|
||||||
title: "Tracer Blocked",
|
title: "Tracer Blocked",
|
||||||
@ -221,8 +221,8 @@ describe.sequential("Jobs API routes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("allows updates for already-enabled tracer links without re-gating", async () => {
|
it("allows updates for already-enabled tracer links without re-gating", async () => {
|
||||||
const { createJob } = await import("../../repositories/jobs");
|
const { createJob } = await import("@server/repositories/jobs");
|
||||||
const { updateJob } = await import("../../repositories/jobs");
|
const { updateJob } = await import("@server/repositories/jobs");
|
||||||
const job = await createJob({
|
const job = await createJob({
|
||||||
source: "manual",
|
source: "manual",
|
||||||
title: "Tracer Already On",
|
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 () => {
|
it("prefers JOBOPS_PUBLIC_BASE_URL over forwarded headers for generate-pdf origin", async () => {
|
||||||
const { createJob } = await import("../../repositories/jobs");
|
const { createJob } = await import("@server/repositories/jobs");
|
||||||
const { generateFinalPdf } = await import("../../pipeline/index");
|
const { generateFinalPdf } = await import("@server/pipeline/index");
|
||||||
const job = await createJob({
|
const job = await createJob({
|
||||||
source: "manual",
|
source: "manual",
|
||||||
title: "Origin Test",
|
title: "Origin Test",
|
||||||
@ -324,7 +324,7 @@ describe.sequential("Jobs API routes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns 409 when patching to a duplicate job URL", async () => {
|
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({
|
const first = await createJob({
|
||||||
source: "manual",
|
source: "manual",
|
||||||
title: "First",
|
title: "First",
|
||||||
@ -354,7 +354,7 @@ describe.sequential("Jobs API routes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("validates job updates and supports skip/delete flow", async () => {
|
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({
|
const job = await createJob({
|
||||||
source: "manual",
|
source: "manual",
|
||||||
title: "Test Role",
|
title: "Test Role",
|
||||||
@ -414,7 +414,7 @@ describe.sequential("Jobs API routes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("runs skip action with partial failures", async () => {
|
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({
|
const discovered = await createJob({
|
||||||
source: "manual",
|
source: "manual",
|
||||||
title: "Discovered Role",
|
title: "Discovered Role",
|
||||||
@ -436,7 +436,7 @@ describe.sequential("Jobs API routes", () => {
|
|||||||
jobUrl: "https://example.com/job/action-applied",
|
jobUrl: "https://example.com/job/action-applied",
|
||||||
jobDescription: "Test description",
|
jobDescription: "Test description",
|
||||||
});
|
});
|
||||||
const { updateJob } = await import("../../repositories/jobs");
|
const { updateJob } = await import("@server/repositories/jobs");
|
||||||
await updateJob(ready.id, { status: "ready" });
|
await updateJob(ready.id, { status: "ready" });
|
||||||
await updateJob(applied.id, { status: "applied" });
|
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 () => {
|
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({
|
const discovered = await createJob({
|
||||||
source: "manual",
|
source: "manual",
|
||||||
title: "New Role",
|
title: "New Role",
|
||||||
@ -481,7 +481,7 @@ describe.sequential("Jobs API routes", () => {
|
|||||||
jobDescription: "Test description",
|
jobDescription: "Test description",
|
||||||
});
|
});
|
||||||
await updateJob(ready.id, { status: "ready" });
|
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;
|
const previousBaseUrl = process.env.JOBOPS_PUBLIC_BASE_URL;
|
||||||
process.env.JOBOPS_PUBLIC_BASE_URL = "https://canonical.jobops.example";
|
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 () => {
|
it("supports legacy move_to_ready endpoint", async () => {
|
||||||
const { createJob } = await import("../../repositories/jobs");
|
const { createJob } = await import("@server/repositories/jobs");
|
||||||
const { processJob } = await import("../../pipeline/index");
|
const { processJob } = await import("@server/pipeline/index");
|
||||||
const job = await createJob({
|
const job = await createJob({
|
||||||
source: "manual",
|
source: "manual",
|
||||||
title: "Legacy Ready Route",
|
title: "Legacy Ready Route",
|
||||||
@ -550,9 +550,9 @@ describe.sequential("Jobs API routes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("runs rescore action with partial failures", async () => {
|
it("runs rescore action with partial failures", async () => {
|
||||||
const { createJob, updateJob } = await import("../../repositories/jobs");
|
const { createJob, updateJob } = await import("@server/repositories/jobs");
|
||||||
const { scoreJobSuitability } = await import("../../services/scorer");
|
const { scoreJobSuitability } = await import("@server/services/scorer");
|
||||||
const { getProfile } = await import("../../services/profile");
|
const { getProfile } = await import("@server/services/profile");
|
||||||
|
|
||||||
vi.mocked(getProfile).mockResolvedValue({});
|
vi.mocked(getProfile).mockResolvedValue({});
|
||||||
vi.mocked(scoreJobSuitability).mockResolvedValue({
|
vi.mocked(scoreJobSuitability).mockResolvedValue({
|
||||||
@ -618,7 +618,7 @@ describe.sequential("Jobs API routes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("streams job action progress with done counters", async () => {
|
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({
|
const discovered = await createJob({
|
||||||
source: "manual",
|
source: "manual",
|
||||||
title: "Discovered Role",
|
title: "Discovered Role",
|
||||||
@ -727,7 +727,7 @@ describe.sequential("Jobs API routes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("applies a job", async () => {
|
it("applies a job", async () => {
|
||||||
const { createJob } = await import("../../repositories/jobs");
|
const { createJob } = await import("@server/repositories/jobs");
|
||||||
const job = await createJob({
|
const job = await createJob({
|
||||||
source: "manual",
|
source: "manual",
|
||||||
title: "Test Role",
|
title: "Test Role",
|
||||||
@ -746,9 +746,9 @@ describe.sequential("Jobs API routes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("rescoring a job updates the suitability fields", async () => {
|
it("rescoring a job updates the suitability fields", async () => {
|
||||||
const { createJob } = await import("../../repositories/jobs");
|
const { createJob } = await import("@server/repositories/jobs");
|
||||||
const { scoreJobSuitability } = await import("../../services/scorer");
|
const { scoreJobSuitability } = await import("@server/services/scorer");
|
||||||
const { getProfile } = await import("../../services/profile");
|
const { getProfile } = await import("@server/services/profile");
|
||||||
|
|
||||||
vi.mocked(getProfile).mockResolvedValue({});
|
vi.mocked(getProfile).mockResolvedValue({});
|
||||||
vi.mocked(scoreJobSuitability).mockResolvedValue({
|
vi.mocked(scoreJobSuitability).mockResolvedValue({
|
||||||
@ -764,7 +764,7 @@ describe.sequential("Jobs API routes", () => {
|
|||||||
jobDescription: "Test description",
|
jobDescription: "Test description",
|
||||||
});
|
});
|
||||||
|
|
||||||
const { updateJob } = await import("../../repositories/jobs");
|
const { updateJob } = await import("@server/repositories/jobs");
|
||||||
await updateJob(job.id, {
|
await updateJob(job.id, {
|
||||||
suitabilityScore: 55,
|
suitabilityScore: 55,
|
||||||
suitabilityReason: "Old fit",
|
suitabilityReason: "Old fit",
|
||||||
@ -785,7 +785,7 @@ describe.sequential("Jobs API routes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("deletes jobs below a score threshold (excluding applied)", async () => {
|
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
|
// Create jobs with different scores and statuses
|
||||||
const lowScoreJob = await createJob({
|
const lowScoreJob = await createJob({
|
||||||
@ -883,7 +883,7 @@ describe.sequential("Jobs API routes", () => {
|
|||||||
|
|
||||||
it("checks visa sponsor status for a job", async () => {
|
it("checks visa sponsor status for a job", async () => {
|
||||||
const { searchSponsors } = await import(
|
const { searchSponsors } = await import(
|
||||||
"../../services/visa-sponsors/index"
|
"@server/services/visa-sponsors/index"
|
||||||
);
|
);
|
||||||
vi.mocked(searchSponsors).mockReturnValue([
|
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({
|
const job = await createJob({
|
||||||
source: "manual",
|
source: "manual",
|
||||||
title: "Sponsored Dev",
|
title: "Sponsored Dev",
|
||||||
@ -915,7 +915,7 @@ describe.sequential("Jobs API routes", () => {
|
|||||||
let jobId: string;
|
let jobId: string;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const { createJob } = await import("../../repositories/jobs");
|
const { createJob } = await import("@server/repositories/jobs");
|
||||||
const job = await createJob({
|
const job = await createJob({
|
||||||
source: "manual",
|
source: "manual",
|
||||||
title: "Tracking Test",
|
title: "Tracking Test",
|
||||||
@ -986,7 +986,7 @@ describe.sequential("Jobs API routes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("manages application tasks", async () => {
|
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 { eq } = await import("drizzle-orm");
|
||||||
const { tasks } = schema;
|
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 { fail, ok, okWithMeta } from "@infra/http";
|
||||||
import { logger } from "@infra/logger";
|
import { logger } from "@infra/logger";
|
||||||
import { sanitizeWebhookPayload } from "@infra/sanitize";
|
import { sanitizeWebhookPayload } from "@infra/sanitize";
|
||||||
import { setupSse, startSseHeartbeat, writeSseData } from "@infra/sse";
|
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 {
|
import {
|
||||||
APPLICATION_OUTCOMES,
|
APPLICATION_OUTCOMES,
|
||||||
APPLICATION_STAGES,
|
APPLICATION_STAGES,
|
||||||
@ -17,40 +52,6 @@ import {
|
|||||||
} from "@shared/types";
|
} from "@shared/types";
|
||||||
import { type Request, type Response, Router } from "express";
|
import { type Request, type Response, Router } from "express";
|
||||||
import { z } from "zod";
|
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();
|
export const jobsRouter = Router();
|
||||||
const JOB_ACTION_CONCURRENCY = 4;
|
const JOB_ACTION_CONCURRENCY = 4;
|
||||||
@ -884,7 +885,7 @@ jobsRouter.get("/:id", async (req: Request, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const job = await jobsRepo.getJobById(req.params.id);
|
const job = await jobsRepo.getJobById(req.params.id);
|
||||||
if (!job) {
|
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 });
|
res.json({ success: true, data: job });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -996,7 +997,7 @@ jobsRouter.patch("/:id/outcome", async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!job) {
|
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 });
|
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);
|
const job = await jobsRepo.getJobById(req.params.id);
|
||||||
if (!job) {
|
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 });
|
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);
|
const job = await jobsRepo.getJobById(req.params.id);
|
||||||
|
|
||||||
if (!job) {
|
if (!job) {
|
||||||
return res.status(404).json({ success: false, error: "Job not found" });
|
return fail(res, notFound("Job not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!job.employer) {
|
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);
|
const job = await jobsRepo.getJobById(req.params.id);
|
||||||
if (!job) {
|
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 });
|
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);
|
const job = await jobsRepo.getJobById(req.params.id);
|
||||||
|
|
||||||
if (!job) {
|
if (!job) {
|
||||||
return res.status(404).json({ success: false, error: "Job not found" });
|
return fail(res, notFound("Job not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const appliedAtDate = new Date();
|
const appliedAtDate = new Date();
|
||||||
@ -1266,7 +1267,7 @@ jobsRouter.post("/:id/apply", async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!updatedJob) {
|
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 });
|
res.json({ success: true, data: updatedJob });
|
||||||
|
|||||||
@ -46,7 +46,9 @@ describe.sequential("Manual jobs API routes", () => {
|
|||||||
});
|
});
|
||||||
expect(badRes.status).toBe(400);
|
expect(badRes.status).toBe(400);
|
||||||
|
|
||||||
const { inferManualJobDetails } = await import("../../services/manualJob");
|
const { inferManualJobDetails } = await import(
|
||||||
|
"@server/services/manualJob"
|
||||||
|
);
|
||||||
vi.mocked(inferManualJobDetails).mockResolvedValue({
|
vi.mocked(inferManualJobDetails).mockResolvedValue({
|
||||||
job: { title: "Backend Engineer", employer: "Acme" },
|
job: { title: "Backend Engineer", employer: "Acme" },
|
||||||
warning: null,
|
warning: null,
|
||||||
@ -63,8 +65,8 @@ describe.sequential("Manual jobs API routes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("imports manual jobs and generates a fallback URL", async () => {
|
it("imports manual jobs and generates a fallback URL", async () => {
|
||||||
const { processJob } = await import("../../pipeline/index");
|
const { processJob } = await import("@server/pipeline/index");
|
||||||
const { scoreJobSuitability } = await import("../../services/scorer");
|
const { scoreJobSuitability } = await import("@server/services/scorer");
|
||||||
vi.mocked(scoreJobSuitability).mockResolvedValue({
|
vi.mocked(scoreJobSuitability).mockResolvedValue({
|
||||||
score: 88,
|
score: 88,
|
||||||
reason: "Strong fit",
|
reason: "Strong fit",
|
||||||
|
|||||||
@ -1,5 +1,12 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
|
import { notFound } from "@infra/errors";
|
||||||
|
import { fail } from "@infra/http";
|
||||||
import { logger } from "@infra/logger";
|
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 {
|
import type {
|
||||||
ApiResponse,
|
ApiResponse,
|
||||||
ManualJobFetchResponse,
|
ManualJobFetchResponse,
|
||||||
@ -8,11 +15,6 @@ import type {
|
|||||||
import { type Request, type Response, Router } from "express";
|
import { type Request, type Response, Router } from "express";
|
||||||
import { JSDOM } from "jsdom";
|
import { JSDOM } from "jsdom";
|
||||||
import { z } from "zod";
|
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();
|
export const manualJobsRouter = Router();
|
||||||
|
|
||||||
@ -252,7 +254,7 @@ manualJobsRouter.post("/import", async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
const processedJob = await jobsRepo.getJobById(createdJob.id);
|
const processedJob = await jobsRepo.getJobById(createdJob.id);
|
||||||
if (!processedJob) {
|
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.
|
// Score asynchronously so the import returns immediately.
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { okWithMeta } from "@infra/http";
|
import { okWithMeta } from "@infra/http";
|
||||||
import { logger } from "@infra/logger";
|
import { logger } from "@infra/logger";
|
||||||
|
import { isDemoMode } from "@server/config/demo";
|
||||||
import { getSetting } from "@server/repositories/settings";
|
import { getSetting } from "@server/repositories/settings";
|
||||||
import { LlmService } from "@server/services/llm/service";
|
import { LlmService } from "@server/services/llm/service";
|
||||||
import { RxResumeClient } from "@server/services/rxresume-client";
|
import { RxResumeClient } from "@server/services/rxresume-client";
|
||||||
@ -9,7 +10,6 @@ import {
|
|||||||
} from "@server/services/rxresume-v4";
|
} from "@server/services/rxresume-v4";
|
||||||
import { resumeDataSchema } from "@shared/rxresume-schema";
|
import { resumeDataSchema } from "@shared/rxresume-schema";
|
||||||
import { type Request, type Response, Router } from "express";
|
import { type Request, type Response, Router } from "express";
|
||||||
import { isDemoMode } from "../../config/demo";
|
|
||||||
|
|
||||||
export const onboardingRouter = Router();
|
export const onboardingRouter = Router();
|
||||||
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ describe.sequential("Pipeline API routes", () => {
|
|||||||
});
|
});
|
||||||
expect(badRun.status).toBe(400);
|
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`, {
|
const runRes = await fetch(`${baseUrl}/api/pipeline/run`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@ -81,7 +81,7 @@ describe.sequential("Pipeline API routes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("accepts cancellation when pipeline is running", async () => {
|
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({
|
vi.mocked(requestPipelineCancel).mockReturnValue({
|
||||||
accepted: true,
|
accepted: true,
|
||||||
pipelineRunId: "run-1",
|
pipelineRunId: "run-1",
|
||||||
|
|||||||
@ -9,23 +9,23 @@ import { fail, ok, okWithMeta } from "@infra/http";
|
|||||||
import { logger } from "@infra/logger";
|
import { logger } from "@infra/logger";
|
||||||
import { runWithRequestContext } from "@infra/request-context";
|
import { runWithRequestContext } from "@infra/request-context";
|
||||||
import { setupSse, startSseHeartbeat, writeSseData } from "@infra/sse";
|
import { setupSse, startSseHeartbeat, writeSseData } from "@infra/sse";
|
||||||
import { PIPELINE_EXTRACTOR_SOURCE_IDS } from "@shared/extractors";
|
import { isDemoMode } from "@server/config/demo";
|
||||||
import type { PipelineStatusResponse } from "@shared/types";
|
|
||||||
import { type Request, type Response, Router } from "express";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { isDemoMode } from "../../config/demo";
|
|
||||||
import {
|
import {
|
||||||
type ExtractorRegistry,
|
type ExtractorRegistry,
|
||||||
getExtractorRegistry,
|
getExtractorRegistry,
|
||||||
} from "../../extractors/registry";
|
} from "@server/extractors/registry";
|
||||||
import {
|
import {
|
||||||
getPipelineStatus,
|
getPipelineStatus,
|
||||||
requestPipelineCancel,
|
requestPipelineCancel,
|
||||||
runPipeline,
|
runPipeline,
|
||||||
subscribeToProgress,
|
subscribeToProgress,
|
||||||
} from "../../pipeline/index";
|
} from "@server/pipeline/index";
|
||||||
import * as pipelineRepo from "../../repositories/pipeline";
|
import * as pipelineRepo from "@server/repositories/pipeline";
|
||||||
import { simulatePipelineRun } from "../../services/demo-simulator";
|
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();
|
export const pipelineRouter = Router();
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type { Server } from "node:http";
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { startServer, stopServer } from "./test-utils";
|
import { startServer, stopServer } from "./test-utils";
|
||||||
|
|
||||||
vi.mock("../../services/post-application/providers", () => ({
|
vi.mock("@server/services/post-application/providers", () => ({
|
||||||
executePostApplicationProviderAction: vi.fn(),
|
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 () => {
|
it("dispatches provider status action and returns unified success contract", async () => {
|
||||||
const { executePostApplicationProviderAction } = await import(
|
const { executePostApplicationProviderAction } = await import(
|
||||||
"../../services/post-application/providers"
|
"@server/services/post-application/providers"
|
||||||
);
|
);
|
||||||
vi.mocked(executePostApplicationProviderAction).mockResolvedValueOnce({
|
vi.mocked(executePostApplicationProviderAction).mockResolvedValueOnce({
|
||||||
provider: "gmail",
|
provider: "gmail",
|
||||||
@ -92,7 +92,7 @@ describe.sequential("Post-Application Provider actions API", () => {
|
|||||||
|
|
||||||
it("defaults to account key 'default' when omitted", async () => {
|
it("defaults to account key 'default' when omitted", async () => {
|
||||||
const { executePostApplicationProviderAction } = await import(
|
const { executePostApplicationProviderAction } = await import(
|
||||||
"../../services/post-application/providers"
|
"@server/services/post-application/providers"
|
||||||
);
|
);
|
||||||
vi.mocked(executePostApplicationProviderAction).mockResolvedValueOnce({
|
vi.mocked(executePostApplicationProviderAction).mockResolvedValueOnce({
|
||||||
provider: "gmail",
|
provider: "gmail",
|
||||||
@ -155,7 +155,7 @@ describe.sequential("Post-Application Provider actions API", () => {
|
|||||||
|
|
||||||
it("maps provider service errors to standardized error responses", async () => {
|
it("maps provider service errors to standardized error responses", async () => {
|
||||||
const { executePostApplicationProviderAction } = await import(
|
const { executePostApplicationProviderAction } = await import(
|
||||||
"../../services/post-application/providers"
|
"@server/services/post-application/providers"
|
||||||
);
|
);
|
||||||
const { AppError } = await import("@infra/errors");
|
const { AppError } = await import("@infra/errors");
|
||||||
vi.mocked(executePostApplicationProviderAction).mockRejectedValueOnce(
|
vi.mocked(executePostApplicationProviderAction).mockRejectedValueOnce(
|
||||||
|
|||||||
@ -2,13 +2,13 @@ import { randomUUID } from "node:crypto";
|
|||||||
import { badRequest, serviceUnavailable, upstreamError } from "@infra/errors";
|
import { badRequest, serviceUnavailable, upstreamError } from "@infra/errors";
|
||||||
import { asyncRoute, fail, ok } from "@infra/http";
|
import { asyncRoute, fail, ok } from "@infra/http";
|
||||||
import { logger } from "@infra/logger";
|
import { logger } from "@infra/logger";
|
||||||
|
import { executePostApplicationProviderAction } from "@server/services/post-application/providers";
|
||||||
import {
|
import {
|
||||||
POST_APPLICATION_PROVIDER_ACTIONS,
|
POST_APPLICATION_PROVIDER_ACTIONS,
|
||||||
POST_APPLICATION_PROVIDERS,
|
POST_APPLICATION_PROVIDERS,
|
||||||
} from "@shared/types";
|
} from "@shared/types";
|
||||||
import { type Request, type Response, Router } from "express";
|
import { type Request, type Response, Router } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { executePostApplicationProviderAction } from "../../services/post-application/providers";
|
|
||||||
|
|
||||||
const providerActionParamsSchema = z.object({
|
const providerActionParamsSchema = z.object({
|
||||||
provider: z.enum(POST_APPLICATION_PROVIDERS),
|
provider: z.enum(POST_APPLICATION_PROVIDERS),
|
||||||
|
|||||||
@ -29,9 +29,9 @@ describe.sequential("Post-Application Review Workflow API", () => {
|
|||||||
message: PostApplicationMessage;
|
message: PostApplicationMessage;
|
||||||
jobId: string;
|
jobId: string;
|
||||||
}> {
|
}> {
|
||||||
const { createJob } = await import("../../repositories/jobs");
|
const { createJob } = await import("@server/repositories/jobs");
|
||||||
const { upsertPostApplicationMessage } = await import(
|
const { upsertPostApplicationMessage } = await import(
|
||||||
"../../repositories/post-application-messages"
|
"@server/repositories/post-application-messages"
|
||||||
);
|
);
|
||||||
|
|
||||||
const job = await createJob({
|
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 () => {
|
it("approves an inbox item and writes stage event", async () => {
|
||||||
const { message, jobId } = await seedPendingMessage();
|
const { message, jobId } = await seedPendingMessage();
|
||||||
const { db, schema } = await import("../../db");
|
const { db, schema } = await import("@server/db");
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${baseUrl}/api/post-application/inbox/${message.id}/approve`,
|
`${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 () => {
|
it("returns conflict on second approve and increments sync-run approval once", async () => {
|
||||||
const { startPostApplicationSyncRun, getPostApplicationSyncRunById } =
|
const { startPostApplicationSyncRun, getPostApplicationSyncRunById } =
|
||||||
await import("../../repositories/post-application-sync-runs");
|
await import("@server/repositories/post-application-sync-runs");
|
||||||
const run = await startPostApplicationSyncRun({
|
const run = await startPostApplicationSyncRun({
|
||||||
provider: "gmail",
|
provider: "gmail",
|
||||||
accountKey: "default",
|
accountKey: "default",
|
||||||
@ -218,7 +218,7 @@ describe.sequential("Post-Application Review Workflow API", () => {
|
|||||||
|
|
||||||
it("lists messages for a sync run", async () => {
|
it("lists messages for a sync run", async () => {
|
||||||
const { startPostApplicationSyncRun } = await import(
|
const { startPostApplicationSyncRun } = await import(
|
||||||
"../../repositories/post-application-sync-runs"
|
"@server/repositories/post-application-sync-runs"
|
||||||
);
|
);
|
||||||
const run = await startPostApplicationSyncRun({
|
const run = await startPostApplicationSyncRun({
|
||||||
provider: "gmail",
|
provider: "gmail",
|
||||||
@ -243,7 +243,7 @@ describe.sequential("Post-Application Review Workflow API", () => {
|
|||||||
const { message, jobId } = await seedPendingMessage({
|
const { message, jobId } = await seedPendingMessage({
|
||||||
stageTarget: "rejected",
|
stageTarget: "rejected",
|
||||||
});
|
});
|
||||||
const { db, schema } = await import("../../db");
|
const { db, schema } = await import("@server/db");
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${baseUrl}/api/post-application/inbox/${message.id}/approve`,
|
`${baseUrl}/api/post-application/inbox/${message.id}/approve`,
|
||||||
@ -274,7 +274,7 @@ describe.sequential("Post-Application Review Workflow API", () => {
|
|||||||
const { message, jobId } = await seedPendingMessage({
|
const { message, jobId } = await seedPendingMessage({
|
||||||
stageTarget: "withdrawn",
|
stageTarget: "withdrawn",
|
||||||
});
|
});
|
||||||
const { db, schema } = await import("../../db");
|
const { db, schema } = await import("@server/db");
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${baseUrl}/api/post-application/inbox/${message.id}/approve`,
|
`${baseUrl}/api/post-application/inbox/${message.id}/approve`,
|
||||||
|
|||||||
@ -1,12 +1,5 @@
|
|||||||
import { badRequest } from "@infra/errors";
|
import { badRequest } from "@infra/errors";
|
||||||
import { asyncRoute, fail, ok } from "@infra/http";
|
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 {
|
import {
|
||||||
approvePostApplicationInboxItem,
|
approvePostApplicationInboxItem,
|
||||||
denyPostApplicationInboxItem,
|
denyPostApplicationInboxItem,
|
||||||
@ -14,7 +7,14 @@ import {
|
|||||||
listPostApplicationReviewRuns,
|
listPostApplicationReviewRuns,
|
||||||
listPostApplicationRunMessages,
|
listPostApplicationRunMessages,
|
||||||
runPostApplicationInboxAction,
|
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({
|
const listQuerySchema = z.object({
|
||||||
provider: z.enum(POST_APPLICATION_PROVIDERS).default("gmail"),
|
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";
|
import { startServer, stopServer } from "./test-utils";
|
||||||
|
|
||||||
// Mock the rxresume-v4 service
|
// Mock the rxresume-v4 service
|
||||||
vi.mock("../../services/rxresume-v4", () => ({
|
vi.mock("@server/services/rxresume-v4", () => ({
|
||||||
getResume: vi.fn(),
|
getResume: vi.fn(),
|
||||||
listResumes: vi.fn(),
|
listResumes: vi.fn(),
|
||||||
RxResumeCredentialsError: class RxResumeCredentialsError extends Error {
|
RxResumeCredentialsError: class RxResumeCredentialsError extends Error {
|
||||||
@ -15,13 +15,13 @@ vi.mock("../../services/rxresume-v4", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock the profile service
|
// Mock the profile service
|
||||||
vi.mock("../../services/profile", () => ({
|
vi.mock("@server/services/profile", () => ({
|
||||||
getProfile: vi.fn(),
|
getProfile: vi.fn(),
|
||||||
clearProfileCache: vi.fn(),
|
clearProfileCache: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock the settings repository
|
// 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>;
|
const original = (await importOriginal()) as Record<string, unknown>;
|
||||||
return {
|
return {
|
||||||
...original,
|
...original,
|
||||||
@ -29,12 +29,12 @@ vi.mock("../../repositories/settings", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
import { getSetting } from "../../repositories/settings";
|
import { getSetting } from "@server/repositories/settings";
|
||||||
import { getProfile } from "../../services/profile";
|
import { getProfile } from "@server/services/profile";
|
||||||
import {
|
import {
|
||||||
getResume,
|
getResume,
|
||||||
RxResumeCredentialsError,
|
RxResumeCredentialsError,
|
||||||
} from "../../services/rxresume-v4";
|
} from "@server/services/rxresume-v4";
|
||||||
|
|
||||||
describe.sequential("Profile API routes", () => {
|
describe.sequential("Profile API routes", () => {
|
||||||
let server: Server;
|
let server: Server;
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { type Request, type Response, Router } from "express";
|
import { isDemoMode } from "@server/config/demo";
|
||||||
import { isDemoMode } from "../../config/demo";
|
import { DEMO_PROJECT_CATALOG } from "@server/config/demo-defaults";
|
||||||
import { DEMO_PROJECT_CATALOG } from "../../config/demo-defaults";
|
import { getSetting } from "@server/repositories/settings";
|
||||||
import { getSetting } from "../../repositories/settings";
|
import { clearProfileCache, getProfile } from "@server/services/profile";
|
||||||
import { clearProfileCache, getProfile } from "../../services/profile";
|
import { extractProjectsFromProfile } from "@server/services/resumeProjects";
|
||||||
import { extractProjectsFromProfile } from "../../services/resumeProjects";
|
|
||||||
import {
|
import {
|
||||||
getResume,
|
getResume,
|
||||||
RxResumeCredentialsError,
|
RxResumeCredentialsError,
|
||||||
} from "../../services/rxresume-v4";
|
} from "@server/services/rxresume-v4";
|
||||||
|
import { type Request, type Response, Router } from "express";
|
||||||
|
|
||||||
export const profileRouter = Router();
|
export const profileRouter = Router();
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { logger } from "@infra/logger";
|
import { logger } from "@infra/logger";
|
||||||
|
import { isDemoMode, sendDemoBlocked } from "@server/config/demo";
|
||||||
import { setBackupSettings } from "@server/services/backup/index";
|
import { setBackupSettings } from "@server/services/backup/index";
|
||||||
import { extractProjectsFromProfile } from "@server/services/resumeProjects";
|
import { extractProjectsFromProfile } from "@server/services/resumeProjects";
|
||||||
import {
|
import {
|
||||||
@ -10,7 +11,6 @@ import { getEffectiveSettings } from "@server/services/settings";
|
|||||||
import { applySettingsUpdates } from "@server/services/settings-update";
|
import { applySettingsUpdates } from "@server/services/settings-update";
|
||||||
import { updateSettingsSchema } from "@shared/settings-schema";
|
import { updateSettingsSchema } from "@shared/settings-schema";
|
||||||
import { type Request, type Response, Router } from "express";
|
import { type Request, type Response, Router } from "express";
|
||||||
import { isDemoMode, sendDemoBlocked } from "../../config/demo";
|
|
||||||
|
|
||||||
export const settingsRouter = Router();
|
export const settingsRouter = Router();
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
|
||||||
vi.mock("../../pipeline/index", () => {
|
vi.mock("@server/pipeline/index", () => {
|
||||||
const progress = {
|
const progress = {
|
||||||
step: "idle",
|
step: "idle",
|
||||||
message: "Ready",
|
message: "Ready",
|
||||||
@ -51,19 +51,19 @@ vi.mock("../../pipeline/index", () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock("../../services/manualJob", () => ({
|
vi.mock("@server/services/manualJob", () => ({
|
||||||
inferManualJobDetails: vi.fn(),
|
inferManualJobDetails: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../services/scorer", () => ({
|
vi.mock("@server/services/scorer", () => ({
|
||||||
scoreJobSuitability: vi.fn(),
|
scoreJobSuitability: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../services/profile", () => ({
|
vi.mock("@server/services/profile", () => ({
|
||||||
getProfile: vi.fn().mockResolvedValue({}),
|
getProfile: vi.fn().mockResolvedValue({}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../services/visa-sponsors/index", () => ({
|
vi.mock("@server/services/visa-sponsors/index", () => ({
|
||||||
getStatus: vi.fn(),
|
getStatus: vi.fn(),
|
||||||
searchSponsors: vi.fn(),
|
searchSponsors: vi.fn(),
|
||||||
getOrganizationDetails: vi.fn(),
|
getOrganizationDetails: vi.fn(),
|
||||||
@ -102,13 +102,13 @@ export async function startServer(options?: {
|
|||||||
...envOverrides,
|
...envOverrides,
|
||||||
};
|
};
|
||||||
|
|
||||||
await import("../../db/migrate");
|
await import("@server/db/migrate");
|
||||||
const { applyStoredEnvOverrides } = await import(
|
const { applyStoredEnvOverrides } = await import(
|
||||||
"../../services/envSettings"
|
"@server/services/envSettings"
|
||||||
);
|
);
|
||||||
const { createApp } = await import("../../app");
|
const { createApp } = await import("../../app");
|
||||||
const { closeDb } = await import("../../db/index");
|
const { closeDb } = await import("@server/db/index");
|
||||||
const { getPipelineStatus } = await import("../../pipeline/index");
|
const { getPipelineStatus } = await import("@server/pipeline/index");
|
||||||
vi.mocked(getPipelineStatus).mockReturnValue({ isRunning: false });
|
vi.mocked(getPipelineStatus).mockReturnValue({ isRunning: false });
|
||||||
|
|
||||||
await applyStoredEnvOverrides();
|
await applyStoredEnvOverrides();
|
||||||
|
|||||||
@ -19,7 +19,7 @@ describe.sequential("Tracer links routes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function seedTracerFixtures() {
|
async function seedTracerFixtures() {
|
||||||
const { db, schema } = await import("../../db");
|
const { db, schema } = await import("@server/db");
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
const jobId = "job-tracer-fixture";
|
const jobId = "job-tracer-fixture";
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { badRequest, notFound } from "@infra/errors";
|
import { badRequest, notFound } from "@infra/errors";
|
||||||
import { asyncRoute, fail, ok } from "@infra/http";
|
import { asyncRoute, fail, ok } from "@infra/http";
|
||||||
import { type Request, type Response, Router } from "express";
|
import * as jobsRepo from "@server/repositories/jobs";
|
||||||
import { z } from "zod";
|
|
||||||
import * as jobsRepo from "../../repositories/jobs";
|
|
||||||
import {
|
import {
|
||||||
getJobTracerLinksAnalytics,
|
getJobTracerLinksAnalytics,
|
||||||
getTracerAnalytics,
|
getTracerAnalytics,
|
||||||
getTracerReadiness,
|
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();
|
export const tracerLinksRouter = Router();
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,7 @@ describe.sequential("Visa sponsors API routes", () => {
|
|||||||
|
|
||||||
it("returns status and surfaces update errors", async () => {
|
it("returns status and surfaces update errors", async () => {
|
||||||
const { getStatus, downloadLatestCsv } = await import(
|
const { getStatus, downloadLatestCsv } = await import(
|
||||||
"../../services/visa-sponsors/index"
|
"@server/services/visa-sponsors/index"
|
||||||
);
|
);
|
||||||
vi.mocked(getStatus).mockReturnValue({
|
vi.mocked(getStatus).mockReturnValue({
|
||||||
lastUpdated: null,
|
lastUpdated: null,
|
||||||
@ -46,7 +46,7 @@ describe.sequential("Visa sponsors API routes", () => {
|
|||||||
|
|
||||||
it("validates search payloads and handles missing organizations", async () => {
|
it("validates search payloads and handles missing organizations", async () => {
|
||||||
const { searchSponsors, getOrganizationDetails } = await import(
|
const { searchSponsors, getOrganizationDetails } = await import(
|
||||||
"../../services/visa-sponsors/index"
|
"@server/services/visa-sponsors/index"
|
||||||
);
|
);
|
||||||
vi.mocked(searchSponsors).mockReturnValue([
|
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 {
|
import type {
|
||||||
ApiResponse,
|
ApiResponse,
|
||||||
VisaSponsorSearchResponse,
|
VisaSponsorSearchResponse,
|
||||||
@ -6,8 +9,6 @@ import type {
|
|||||||
import { type Request, type Response, Router } from "express";
|
import { type Request, type Response, Router } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import * as visaSponsors from "../../services/visa-sponsors/index";
|
|
||||||
|
|
||||||
export const visaSponsorsRouter = Router();
|
export const visaSponsorsRouter = Router();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -74,9 +75,7 @@ visaSponsorsRouter.get(
|
|||||||
const entries = visaSponsors.getOrganizationDetails(name);
|
const entries = visaSponsors.getOrganizationDetails(name);
|
||||||
|
|
||||||
if (entries.length === 0) {
|
if (entries.length === 0) {
|
||||||
return res
|
return fail(res, notFound("Organization not found"));
|
||||||
.status(404)
|
|
||||||
.json({ success: false, error: "Organization not found" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
|
|||||||
@ -2,10 +2,10 @@ import { unauthorized } from "@infra/errors";
|
|||||||
import { fail, okWithMeta } from "@infra/http";
|
import { fail, okWithMeta } from "@infra/http";
|
||||||
import { logger } from "@infra/logger";
|
import { logger } from "@infra/logger";
|
||||||
import { runWithRequestContext } from "@infra/request-context";
|
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 { 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();
|
export const webhookRouter = Router();
|
||||||
|
|
||||||
|
|||||||
@ -3,15 +3,15 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
import { getProgress, resetProgress } from "../progress";
|
import { getProgress, resetProgress } from "../progress";
|
||||||
import { discoverJobsStep } from "./discover-jobs";
|
import { discoverJobsStep } from "./discover-jobs";
|
||||||
|
|
||||||
vi.mock("../../repositories/settings", () => ({
|
vi.mock("@server/repositories/settings", () => ({
|
||||||
getAllSettings: vi.fn(),
|
getAllSettings: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../repositories/jobs", () => ({
|
vi.mock("@server/repositories/jobs", () => ({
|
||||||
getAllJobUrls: vi.fn().mockResolvedValue([]),
|
getAllJobUrls: vi.fn().mockResolvedValue([]),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../extractors/registry", () => ({
|
vi.mock("@server/extractors/registry", () => ({
|
||||||
getExtractorRegistry: vi.fn(),
|
getExtractorRegistry: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -33,8 +33,8 @@ describe("discoverJobsStep", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("aggregates source errors for enabled sources", async () => {
|
it("aggregates source errors for enabled sources", async () => {
|
||||||
const settingsRepo = await import("../../repositories/settings");
|
const settingsRepo = await import("@server/repositories/settings");
|
||||||
const registryModule = await import("../../extractors/registry");
|
const registryModule = await import("@server/extractors/registry");
|
||||||
|
|
||||||
const jobspyManifest = {
|
const jobspyManifest = {
|
||||||
id: "jobspy",
|
id: "jobspy",
|
||||||
@ -93,8 +93,8 @@ describe("discoverJobsStep", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("throws when all enabled sources fail", async () => {
|
it("throws when all enabled sources fail", async () => {
|
||||||
const settingsRepo = await import("../../repositories/settings");
|
const settingsRepo = await import("@server/repositories/settings");
|
||||||
const registryModule = await import("../../extractors/registry");
|
const registryModule = await import("@server/extractors/registry");
|
||||||
|
|
||||||
const ukvisaManifest = {
|
const ukvisaManifest = {
|
||||||
id: "ukvisajobs",
|
id: "ukvisajobs",
|
||||||
@ -130,8 +130,8 @@ describe("discoverJobsStep", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("throws when all requested sources are incompatible for country", async () => {
|
it("throws when all requested sources are incompatible for country", async () => {
|
||||||
const settingsRepo = await import("../../repositories/settings");
|
const settingsRepo = await import("@server/repositories/settings");
|
||||||
const registryModule = await import("../../extractors/registry");
|
const registryModule = await import("@server/extractors/registry");
|
||||||
|
|
||||||
vi.mocked(settingsRepo.getAllSettings).mockResolvedValue({
|
vi.mocked(settingsRepo.getAllSettings).mockResolvedValue({
|
||||||
searchTerms: JSON.stringify(["engineer"]),
|
searchTerms: JSON.stringify(["engineer"]),
|
||||||
@ -157,8 +157,8 @@ describe("discoverJobsStep", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("does not throw when no sources are requested", async () => {
|
it("does not throw when no sources are requested", async () => {
|
||||||
const settingsRepo = await import("../../repositories/settings");
|
const settingsRepo = await import("@server/repositories/settings");
|
||||||
const registryModule = await import("../../extractors/registry");
|
const registryModule = await import("@server/extractors/registry");
|
||||||
|
|
||||||
vi.mocked(settingsRepo.getAllSettings).mockResolvedValue({
|
vi.mocked(settingsRepo.getAllSettings).mockResolvedValue({
|
||||||
searchTerms: JSON.stringify(["engineer"]),
|
searchTerms: JSON.stringify(["engineer"]),
|
||||||
@ -183,8 +183,8 @@ describe("discoverJobsStep", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("drops discovered jobs when employer matches blocked company keywords", async () => {
|
it("drops discovered jobs when employer matches blocked company keywords", async () => {
|
||||||
const settingsRepo = await import("../../repositories/settings");
|
const settingsRepo = await import("@server/repositories/settings");
|
||||||
const registryModule = await import("../../extractors/registry");
|
const registryModule = await import("@server/extractors/registry");
|
||||||
|
|
||||||
const jobspyManifest = {
|
const jobspyManifest = {
|
||||||
id: "jobspy",
|
id: "jobspy",
|
||||||
@ -236,8 +236,8 @@ describe("discoverJobsStep", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("applies shared city filtering for sources without native city filtering", async () => {
|
it("applies shared city filtering for sources without native city filtering", async () => {
|
||||||
const settingsRepo = await import("../../repositories/settings");
|
const settingsRepo = await import("@server/repositories/settings");
|
||||||
const registryModule = await import("../../extractors/registry");
|
const registryModule = await import("@server/extractors/registry");
|
||||||
|
|
||||||
const gradcrackerManifest = {
|
const gradcrackerManifest = {
|
||||||
id: "gradcracker",
|
id: "gradcracker",
|
||||||
@ -313,9 +313,9 @@ describe("discoverJobsStep", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("tracks source completion counters across source transitions", async () => {
|
it("tracks source completion counters across source transitions", async () => {
|
||||||
const settingsRepo = await import("../../repositories/settings");
|
const settingsRepo = await import("@server/repositories/settings");
|
||||||
const jobsRepo = await import("../../repositories/jobs");
|
const jobsRepo = await import("@server/repositories/jobs");
|
||||||
const registryModule = await import("../../extractors/registry");
|
const registryModule = await import("@server/extractors/registry");
|
||||||
|
|
||||||
const jobspyManifest = {
|
const jobspyManifest = {
|
||||||
id: "jobspy",
|
id: "jobspy",
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
import { logger } from "@infra/logger";
|
import { logger } from "@infra/logger";
|
||||||
import { sanitizeUnknown } from "@infra/sanitize";
|
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 {
|
import {
|
||||||
formatCountryLabel,
|
formatCountryLabel,
|
||||||
isSourceAllowedForCountry,
|
isSourceAllowedForCountry,
|
||||||
@ -12,10 +16,6 @@ import {
|
|||||||
shouldApplyStrictCityFilter,
|
shouldApplyStrictCityFilter,
|
||||||
} from "@shared/search-cities.js";
|
} from "@shared/search-cities.js";
|
||||||
import type { CreateJobInput, PipelineConfig } from "@shared/types";
|
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";
|
import { type CrawlSource, progressHelpers, updateProgress } from "../progress";
|
||||||
|
|
||||||
const DISCOVERY_CONCURRENCY = 3;
|
const DISCOVERY_CONCURRENCY = 3;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { logger } from "@infra/logger";
|
import { logger } from "@infra/logger";
|
||||||
|
import * as jobsRepo from "@server/repositories/jobs";
|
||||||
import type { CreateJobInput } from "@shared/types";
|
import type { CreateJobInput } from "@shared/types";
|
||||||
import * as jobsRepo from "../../repositories/jobs";
|
|
||||||
import { progressHelpers } from "../progress";
|
import { progressHelpers } from "../progress";
|
||||||
|
|
||||||
export async function importJobsStep(args: {
|
export async function importJobsStep(args: {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { logger } from "@infra/logger";
|
import { logger } from "@infra/logger";
|
||||||
import { getProfile } from "../../services/profile";
|
import { getProfile } from "@server/services/profile";
|
||||||
|
|
||||||
export async function loadProfileStep(): Promise<Record<string, unknown>> {
|
export async function loadProfileStep(): Promise<Record<string, unknown>> {
|
||||||
logger.info("Loading profile");
|
logger.info("Loading profile");
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { logger } from "@infra/logger";
|
import { logger } from "@infra/logger";
|
||||||
import { sanitizeWebhookPayload } from "@infra/sanitize";
|
import { sanitizeWebhookPayload } from "@infra/sanitize";
|
||||||
import * as settingsRepo from "../../repositories/settings";
|
import * as settingsRepo from "@server/repositories/settings";
|
||||||
|
|
||||||
export async function notifyPipelineWebhookStep(
|
export async function notifyPipelineWebhookStep(
|
||||||
event: "pipeline.completed" | "pipeline.failed",
|
event: "pipeline.completed" | "pipeline.failed",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { logger } from "@infra/logger";
|
import { logger } from "@infra/logger";
|
||||||
import { asyncPool } from "../../utils/async-pool";
|
import { asyncPool } from "@server/utils/async-pool";
|
||||||
import { progressHelpers, updateProgress } from "../progress";
|
import { progressHelpers, updateProgress } from "../progress";
|
||||||
import type { ScoredJob } from "./types";
|
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(),
|
getUnscoredDiscoveredJobs: vi.fn(),
|
||||||
updateJob: vi.fn(),
|
updateJob: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../repositories/settings", () => ({
|
vi.mock("@server/repositories/settings", () => ({
|
||||||
getSetting: vi.fn(),
|
getSetting: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../services/scorer", () => ({
|
vi.mock("@server/services/scorer", () => ({
|
||||||
scoreJobSuitability: vi.fn(),
|
scoreJobSuitability: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../services/visa-sponsors/index", () => ({
|
vi.mock("@server/services/visa-sponsors/index", () => ({
|
||||||
searchSponsors: vi.fn(),
|
searchSponsors: vi.fn(),
|
||||||
calculateSponsorMatchSummary: vi.fn(),
|
calculateSponsorMatchSummary: vi.fn(),
|
||||||
}));
|
}));
|
||||||
@ -40,10 +40,10 @@ describe("scoreJobsStep auto-skip behavior", () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
const jobsRepo = await import("../../repositories/jobs");
|
const jobsRepo = await import("@server/repositories/jobs");
|
||||||
const settingsRepo = await import("../../repositories/settings");
|
const settingsRepo = await import("@server/repositories/settings");
|
||||||
const scorer = await import("../../services/scorer");
|
const scorer = await import("@server/services/scorer");
|
||||||
const visaSponsors = await import("../../services/visa-sponsors/index");
|
const visaSponsors = await import("@server/services/visa-sponsors/index");
|
||||||
|
|
||||||
vi.mocked(jobsRepo.getUnscoredDiscoveredJobs).mockResolvedValue([
|
vi.mocked(jobsRepo.getUnscoredDiscoveredJobs).mockResolvedValue([
|
||||||
createJob({
|
createJob({
|
||||||
@ -68,8 +68,8 @@ describe("scoreJobsStep auto-skip behavior", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("auto-skips jobs when score is below threshold", async () => {
|
it("auto-skips jobs when score is below threshold", async () => {
|
||||||
const settingsRepo = await import("../../repositories/settings");
|
const settingsRepo = await import("@server/repositories/settings");
|
||||||
const jobsRepo = await import("../../repositories/jobs");
|
const jobsRepo = await import("@server/repositories/jobs");
|
||||||
const { logger } = await import("@infra/logger");
|
const { logger } = await import("@infra/logger");
|
||||||
|
|
||||||
vi.mocked(settingsRepo.getSetting).mockResolvedValue("50");
|
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 () => {
|
it("does not auto-skip jobs when score equals threshold", async () => {
|
||||||
const settingsRepo = await import("../../repositories/settings");
|
const settingsRepo = await import("@server/repositories/settings");
|
||||||
const jobsRepo = await import("../../repositories/jobs");
|
const jobsRepo = await import("@server/repositories/jobs");
|
||||||
const scorer = await import("../../services/scorer");
|
const scorer = await import("@server/services/scorer");
|
||||||
const { logger } = await import("@infra/logger");
|
const { logger } = await import("@infra/logger");
|
||||||
|
|
||||||
vi.mocked(settingsRepo.getSetting).mockResolvedValue("50");
|
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 () => {
|
it("does not auto-skip when threshold setting is null", async () => {
|
||||||
const settingsRepo = await import("../../repositories/settings");
|
const settingsRepo = await import("@server/repositories/settings");
|
||||||
const jobsRepo = await import("../../repositories/jobs");
|
const jobsRepo = await import("@server/repositories/jobs");
|
||||||
|
|
||||||
vi.mocked(settingsRepo.getSetting).mockResolvedValue(null);
|
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 () => {
|
it("does not auto-skip when threshold setting is NaN", async () => {
|
||||||
const settingsRepo = await import("../../repositories/settings");
|
const settingsRepo = await import("@server/repositories/settings");
|
||||||
const jobsRepo = await import("../../repositories/jobs");
|
const jobsRepo = await import("@server/repositories/jobs");
|
||||||
|
|
||||||
vi.mocked(settingsRepo.getSetting).mockResolvedValue("not-a-number");
|
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 () => {
|
it("never auto-skips applied jobs even when score is below threshold", async () => {
|
||||||
const settingsRepo = await import("../../repositories/settings");
|
const settingsRepo = await import("@server/repositories/settings");
|
||||||
const jobsRepo = await import("../../repositories/jobs");
|
const jobsRepo = await import("@server/repositories/jobs");
|
||||||
const { logger } = await import("@infra/logger");
|
const { logger } = await import("@infra/logger");
|
||||||
|
|
||||||
vi.mocked(settingsRepo.getSetting).mockResolvedValue("50");
|
vi.mocked(settingsRepo.getSetting).mockResolvedValue("50");
|
||||||
@ -185,8 +185,8 @@ describe("scoreJobsStep auto-skip behavior", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("scores multiple jobs and reports completion progress", async () => {
|
it("scores multiple jobs and reports completion progress", async () => {
|
||||||
const jobsRepo = await import("../../repositories/jobs");
|
const jobsRepo = await import("@server/repositories/jobs");
|
||||||
const scorer = await import("../../services/scorer");
|
const scorer = await import("@server/services/scorer");
|
||||||
const { progressHelpers } = await import("../progress");
|
const { progressHelpers } = await import("../progress");
|
||||||
|
|
||||||
vi.mocked(jobsRepo.getUnscoredDiscoveredJobs).mockResolvedValue([
|
vi.mocked(jobsRepo.getUnscoredDiscoveredJobs).mockResolvedValue([
|
||||||
@ -217,8 +217,8 @@ describe("scoreJobsStep auto-skip behavior", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("stops before processing when cancellation is requested", async () => {
|
it("stops before processing when cancellation is requested", async () => {
|
||||||
const jobsRepo = await import("../../repositories/jobs");
|
const jobsRepo = await import("@server/repositories/jobs");
|
||||||
const scorer = await import("../../services/scorer");
|
const scorer = await import("@server/services/scorer");
|
||||||
|
|
||||||
vi.mocked(jobsRepo.getUnscoredDiscoveredJobs).mockResolvedValue([
|
vi.mocked(jobsRepo.getUnscoredDiscoveredJobs).mockResolvedValue([
|
||||||
createJob({
|
createJob({
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { logger } from "@infra/logger";
|
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 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 { progressHelpers, updateProgress } from "../progress";
|
||||||
import type { ScoredJob } from "./types";
|
import type { ScoredJob } from "./types";
|
||||||
|
|
||||||
|
|||||||
@ -6,11 +6,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
import * as backup from "./index";
|
import * as backup from "./index";
|
||||||
|
|
||||||
// Mock the dataDir module
|
// Mock the dataDir module
|
||||||
vi.mock("../../config/dataDir", () => ({
|
vi.mock("@server/config/dataDir", () => ({
|
||||||
getDataDir: vi.fn(),
|
getDataDir: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { getDataDir } from "../../config/dataDir";
|
import { getDataDir } from "@server/config/dataDir";
|
||||||
|
|
||||||
describe("Backup Service", () => {
|
describe("Backup Service", () => {
|
||||||
let tempDir: string;
|
let tempDir: string;
|
||||||
|
|||||||
@ -8,10 +8,10 @@
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import type { FileHandle } from "node:fs/promises";
|
import type { FileHandle } from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { getDataDir } from "@server/config/dataDir";
|
||||||
|
import { createScheduler } from "@server/utils/scheduler";
|
||||||
import type { BackupInfo } from "@shared/types";
|
import type { BackupInfo } from "@shared/types";
|
||||||
import Database from "better-sqlite3";
|
import Database from "better-sqlite3";
|
||||||
import { getDataDir } from "../../config/dataDir";
|
|
||||||
import { createScheduler } from "../../utils/scheduler";
|
|
||||||
|
|
||||||
const DB_FILENAME = "jobs.db";
|
const DB_FILENAME = "jobs.db";
|
||||||
const AUTO_BACKUP_PREFIX = "jobs_";
|
const AUTO_BACKUP_PREFIX = "jobs_";
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
|
import type { AppError } from "@infra/errors";
|
||||||
import { createJob } from "@shared/testing/factories";
|
import { createJob } from "@shared/testing/factories";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import type { AppError } from "../infra/errors";
|
|
||||||
import { buildJobChatPromptContext } from "./ghostwriter-context";
|
import { buildJobChatPromptContext } from "./ghostwriter-context";
|
||||||
|
|
||||||
vi.mock("../repositories/jobs", () => ({
|
vi.mock("../repositories/jobs", () => ({
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
import { badRequest, notFound } from "@infra/errors";
|
||||||
import { logger } from "@infra/logger";
|
import { logger } from "@infra/logger";
|
||||||
import { sanitizeUnknown } from "@infra/sanitize";
|
import { sanitizeUnknown } from "@infra/sanitize";
|
||||||
import type { Job, ResumeProfile } from "@shared/types";
|
import type { Job, ResumeProfile } from "@shared/types";
|
||||||
import { badRequest, notFound } from "../infra/errors";
|
|
||||||
import * as jobsRepo from "../repositories/jobs";
|
import * as jobsRepo from "../repositories/jobs";
|
||||||
import { getProfile } from "./profile";
|
import { getProfile } from "./profile";
|
||||||
import { getEffectiveSettings } from "./settings";
|
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 {
|
import {
|
||||||
badRequest,
|
badRequest,
|
||||||
conflict,
|
conflict,
|
||||||
notFound,
|
notFound,
|
||||||
requestTimeout,
|
requestTimeout,
|
||||||
upstreamError,
|
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 jobChatRepo from "../repositories/ghostwriter";
|
||||||
import * as settingsRepo from "../repositories/settings";
|
import * as settingsRepo from "../repositories/settings";
|
||||||
import { buildJobChatPromptContext } from "./ghostwriter-context";
|
import { buildJobChatPromptContext } from "./ghostwriter-context";
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
import { logger } from "@infra/logger";
|
import { logger } from "@infra/logger";
|
||||||
|
|
||||||
export function parseJsonContent<T>(content: string, jobId?: string): T {
|
export function stripMarkdownCodeFences(content: string): string {
|
||||||
let candidate = content.trim();
|
return content
|
||||||
|
|
||||||
candidate = candidate
|
|
||||||
.replace(/```(?:json|JSON)?\s*/g, "")
|
.replace(/```(?:json|JSON)?\s*/g, "")
|
||||||
.replace(/```/g, "")
|
.replace(/```/g, "")
|
||||||
.trim();
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseJsonContent<T>(content: string, jobId?: string): T {
|
||||||
|
let candidate = stripMarkdownCodeFences(content);
|
||||||
|
|
||||||
const firstBrace = candidate.indexOf("{");
|
const firstBrace = candidate.indexOf("{");
|
||||||
const lastBrace = candidate.lastIndexOf("}");
|
const lastBrace = candidate.lastIndexOf("}");
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import type {
|
|||||||
PostApplicationRouterStageTarget,
|
PostApplicationRouterStageTarget,
|
||||||
} from "@shared/types";
|
} from "@shared/types";
|
||||||
import { POST_APPLICATION_ROUTER_STAGE_TARGETS } 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;
|
export const ROUTER_EMAIL_CHAR_LIMIT = 12_000;
|
||||||
|
|
||||||
@ -91,7 +92,7 @@ export function minifyActiveJobs(jobs: Job[]): Array<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeJobPromptValue(value: string): string {
|
function sanitizeJobPromptValue(value: string): string {
|
||||||
return value.replace(/\s+/g, " ").trim();
|
return normalizeWhitespace(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildIndexedActiveJobs(
|
export function buildIndexedActiveJobs(
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { requestTimeout } from "@infra/errors";
|
import { requestTimeout } from "@infra/errors";
|
||||||
|
import { normalizeWhitespace } from "@shared/utils/string";
|
||||||
import { convert } from "html-to-text";
|
import { convert } from "html-to-text";
|
||||||
|
|
||||||
export const GMAIL_HTTP_TIMEOUT_MS = 15_000;
|
export const GMAIL_HTTP_TIMEOUT_MS = 15_000;
|
||||||
@ -313,7 +314,7 @@ function cleanEmailHtmlForLlm(htmlContent: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeChunkForDedup(value: string): string {
|
function normalizeChunkForDedup(value: string): string {
|
||||||
return value.replace(/\s+/g, " ").trim().toLowerCase();
|
return normalizeWhitespace(value).toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeBase64Url(value: string): string {
|
function decodeBase64Url(value: string): string {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import type {
|
|||||||
ResumeProjectCatalogItem,
|
ResumeProjectCatalogItem,
|
||||||
ResumeProjectsSettings,
|
ResumeProjectsSettings,
|
||||||
} from "@shared/types";
|
} from "@shared/types";
|
||||||
|
import { stripHtmlTags } from "@shared/utils/string";
|
||||||
|
|
||||||
type ResumeProjectSelectionItem = ResumeProjectCatalogItem & {
|
type ResumeProjectSelectionItem = ResumeProjectCatalogItem & {
|
||||||
summaryText: string;
|
summaryText: string;
|
||||||
@ -156,8 +157,7 @@ export function resolveResumeProjectsSettings(args: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function stripHtml(input: string): string {
|
export function stripHtml(input: string): string {
|
||||||
const withoutTags = input.replace(/<[^>]*>/g, " ");
|
return stripHtmlTags(input);
|
||||||
return withoutTags.replace(/\s+/g, " ").trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function uniqueStrings(values: string[]): string[] {
|
function uniqueStrings(values: string[]): string[] {
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
// - The v5 client should be a drop-in replacement in the future.
|
// - The v5 client should be a drop-in replacement in the future.
|
||||||
|
|
||||||
import type { ResumeData } from "@shared/rxresume-schema";
|
import type { ResumeData } from "@shared/rxresume-schema";
|
||||||
|
import { normalizeWhitespace } from "@shared/utils/string";
|
||||||
|
|
||||||
type AnyObj = Record<string, unknown>;
|
type AnyObj = Record<string, unknown>;
|
||||||
const MAX_ERROR_SNIPPET = 300;
|
const MAX_ERROR_SNIPPET = 300;
|
||||||
@ -457,6 +458,6 @@ export class RxResumeClient {
|
|||||||
|
|
||||||
function sanitizeResponseSnippet(text: string): string {
|
function sanitizeResponseSnippet(text: string): string {
|
||||||
if (!text) return "";
|
if (!text) return "";
|
||||||
const compact = text.replace(/\s+/g, " ").trim();
|
const compact = normalizeWhitespace(text);
|
||||||
return compact.slice(0, MAX_ERROR_SNIPPET);
|
return compact.slice(0, MAX_ERROR_SNIPPET);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import type { Job } from "@shared/types";
|
|||||||
import { getSetting } from "../repositories/settings";
|
import { getSetting } from "../repositories/settings";
|
||||||
import { LlmService } from "./llm/service";
|
import { LlmService } from "./llm/service";
|
||||||
import type { JsonSchemaDefinition } from "./llm/types";
|
import type { JsonSchemaDefinition } from "./llm/types";
|
||||||
|
import { stripMarkdownCodeFences } from "./llm/utils/json";
|
||||||
import { getEffectiveSettings } from "./settings";
|
import { getEffectiveSettings } from "./settings";
|
||||||
|
|
||||||
interface SuitabilityResult {
|
interface SuitabilityResult {
|
||||||
@ -162,10 +163,7 @@ export function parseJsonFromContent(
|
|||||||
let candidate = content.trim();
|
let candidate = content.trim();
|
||||||
|
|
||||||
// Step 1: Remove markdown code fences (with or without language specifier)
|
// Step 1: Remove markdown code fences (with or without language specifier)
|
||||||
candidate = candidate
|
candidate = stripMarkdownCodeFences(candidate);
|
||||||
.replace(/```(?:json|JSON)?\s*/g, "")
|
|
||||||
.replace(/```/g, "")
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
// Step 2: Try to extract JSON object if there's surrounding text
|
// Step 2: Try to extract JSON object if there's surrounding text
|
||||||
const jsonMatch = candidate.match(/\{[\s\S]*\}/);
|
const jsonMatch = candidate.match(/\{[\s\S]*\}/);
|
||||||
|
|||||||
@ -6,8 +6,14 @@
|
|||||||
|
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { getDataDir } from "../../config/dataDir";
|
import { getDataDir } from "@server/config/dataDir";
|
||||||
import { createScheduler } from "../../utils/scheduler";
|
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");
|
const DATA_DIR = path.join(getDataDir(), "visa-sponsors");
|
||||||
|
|
||||||
@ -16,28 +22,8 @@ if (!fs.existsSync(DATA_DIR)) {
|
|||||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VisaSponsor {
|
export type { VisaSponsor, VisaSponsorSearchResult };
|
||||||
organisationName: string;
|
export type VisaSponsorStatus = VisaSponsorStatusResponse;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Common company suffixes to strip during comparison
|
// Common company suffixes to strip during comparison
|
||||||
const COMPANY_SUFFIXES = [
|
const COMPANY_SUFFIXES = [
|
||||||
@ -86,7 +72,7 @@ export function normalizeCompanyName(name: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Collapse whitespace
|
// Collapse whitespace
|
||||||
normalized = normalized.replace(/\s+/g, " ").trim();
|
normalized = normalizeWhitespace(normalized);
|
||||||
|
|
||||||
return 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