feat(extractors): expand catalog, smoke coverage, and sourcing docs
CI / Linting (Biome) (push) Failing after 40s
CI / Tests (push) Successful in 5m54s
CI / Type Check (adzuna-extractor) (push) Successful in 1m8s
CI / Type Check (gradcracker-extractor) (push) Successful in 1m11s
CI / Type Check (hiringcafe-extractor) (push) Successful in 1m8s
CI / Type Check (orchestrator) (push) Successful in 1m23s
CI / Type Check (startupjobs-extractor) (push) Successful in 1m6s
CI / Type Check (ukvisajobs-extractor) (push) Successful in 1m7s
CI / Documentation (push) Successful in 1m54s
CI / Linting (Biome) (push) Failing after 40s
CI / Tests (push) Successful in 5m54s
CI / Type Check (adzuna-extractor) (push) Successful in 1m8s
CI / Type Check (gradcracker-extractor) (push) Successful in 1m11s
CI / Type Check (hiringcafe-extractor) (push) Successful in 1m8s
CI / Type Check (orchestrator) (push) Successful in 1m23s
CI / Type Check (startupjobs-extractor) (push) Successful in 1m6s
CI / Type Check (ukvisajobs-extractor) (push) Successful in 1m7s
CI / Documentation (push) Successful in 1m54s
Adds Arc.dev, BC T-Net, Eluta, iCIMS tenants, QAJobsBoard, and SmartRecruiters manifests with registry/settings/UI wiring; registers full extractor list in smoke-extractors and documents supplementary board access paths. Aligns Careerjet v4 with the url query parameter and fixes strict typing in QAJobsBoard. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+215
-76
@@ -1,8 +1,14 @@
|
||||
/**
|
||||
* Tiny smoke-test for new extractors: imports each manifest, runs it with a
|
||||
* minimal context, and prints the count of mapped jobs + a few samples.
|
||||
* Smoke-test helper for extractor manifests: imports each manifest, runs it with a
|
||||
* minimal context, and prints mapped job counts + a sample row.
|
||||
*
|
||||
* Run from repo root: npx tsx scripts/smoke-extractors.ts [comma,separated,ids]
|
||||
* Run from repo root:
|
||||
* npx tsx scripts/smoke-extractors.ts
|
||||
* npx tsx scripts/smoke-extractors.ts arcdev,icims
|
||||
* npx tsx scripts/smoke-extractors.ts indeed # alias → `jobspy` (same manifest)
|
||||
*
|
||||
* Keep `ALL_TARGETS` aligned with every shipped manifest under each
|
||||
* `extractors/<name>/` package (`manifest.ts` or `src/manifest.ts`).
|
||||
*
|
||||
* Loads repo-root `.env` so keyed extractors match orchestrator behavior (plain
|
||||
* `tsx` does not read `.env` automatically).
|
||||
@@ -14,7 +20,7 @@ import { config as loadEnv } from "dotenv";
|
||||
import type {
|
||||
ExtractorManifest,
|
||||
ExtractorRuntimeContext,
|
||||
} from "../shared/src/types/extractors";
|
||||
} from "job-ops-shared/types/extractors";
|
||||
|
||||
const repoRoot = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
@@ -22,54 +28,56 @@ const repoRoot = path.resolve(
|
||||
);
|
||||
loadEnv({ path: path.join(repoRoot, ".env") });
|
||||
|
||||
/** Left column width for log alignment (longest pipeline source id today). */
|
||||
const ID_COL = 15;
|
||||
|
||||
/** JobSpy serves Indeed / LinkedIn / Glassdoor; CLI filter accepts those ids as aliases. */
|
||||
const JOBSPY_SITE_IDS = ["indeed", "linkedin", "glassdoor"] as const;
|
||||
|
||||
function expandSmokeFilter(ids: Set<string>): Set<string> {
|
||||
const next = new Set(ids);
|
||||
for (const site of JOBSPY_SITE_IDS) {
|
||||
if (next.has(site)) {
|
||||
next.add("jobspy");
|
||||
break;
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
interface Target {
|
||||
id: string;
|
||||
importPath: string;
|
||||
needs?: string[]; // env vars required to run; skipped if missing
|
||||
settings?: Record<string, string>;
|
||||
/** When set, replaces the default smoke search terms (use [] for sources that filter client-side). */
|
||||
searchTerms?: string[];
|
||||
/** Geography passed as `selectedCountry` (must match what each extractor expects). */
|
||||
selectedCountry?: string;
|
||||
}
|
||||
|
||||
const ALL_TARGETS: Target[] = [
|
||||
{
|
||||
id: "jobicy",
|
||||
importPath: "../extractors/jobicy/manifest",
|
||||
settings: { jobicyMaxJobsPerTerm: "10" },
|
||||
},
|
||||
{
|
||||
id: "themuse",
|
||||
importPath: "../extractors/themuse/manifest",
|
||||
settings: { themuseMaxJobsPerTerm: "10" },
|
||||
},
|
||||
{
|
||||
id: "usajobs",
|
||||
importPath: "../extractors/usajobs/manifest",
|
||||
needs: ["USAJOBS_API_KEY", "USAJOBS_USER_AGENT"],
|
||||
settings: { usajobsMaxJobsPerTerm: "10" },
|
||||
},
|
||||
{
|
||||
id: "jooble",
|
||||
importPath: "../extractors/jooble/manifest",
|
||||
needs: ["JOOBLE_API_KEY"],
|
||||
settings: { joobleMaxJobsPerTerm: "10" },
|
||||
},
|
||||
{
|
||||
id: "careerjet",
|
||||
importPath: "../extractors/careerjet/manifest",
|
||||
needs: ["CAREERJET_AFFID", "CAREERJET_REFERER", "CAREERJET_USER_IP"],
|
||||
settings: { careerjetMaxJobsPerTerm: "10" },
|
||||
},
|
||||
{
|
||||
id: "reed",
|
||||
importPath: "../extractors/reed/manifest",
|
||||
needs: ["REED_API_KEY"],
|
||||
settings: { reedMaxJobsPerTerm: "10" },
|
||||
},
|
||||
{
|
||||
id: "lever",
|
||||
importPath: "../extractors/lever/manifest",
|
||||
id: "adzuna",
|
||||
importPath: "../extractors/adzuna/manifest",
|
||||
needs: ["ADZUNA_APP_ID", "ADZUNA_APP_KEY"],
|
||||
selectedCountry: "United States",
|
||||
settings: {
|
||||
// Known active public Lever board used purely as a connectivity check.
|
||||
leverCompanies: JSON.stringify(["palantir", "netflix"]),
|
||||
adzunaMaxJobsPerTerm: "10",
|
||||
searchCities: "United States",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "arbeitnow",
|
||||
importPath: "../extractors/arbeitnow/manifest",
|
||||
settings: { arbeitnowMaxJobsPerTerm: "10" },
|
||||
},
|
||||
{
|
||||
id: "arcdev",
|
||||
importPath: "../extractors/arcdev/manifest",
|
||||
settings: {
|
||||
arcRemoteJobsPaths: JSON.stringify(["/remote-jobs/playwright"]),
|
||||
arcMaxJobsPerPath: "20",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -79,6 +87,40 @@ const ALL_TARGETS: Target[] = [
|
||||
ashbyCompanies: JSON.stringify(["ramp", "linear"]),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "bctenet",
|
||||
importPath: "../extractors/bctenet/manifest",
|
||||
selectedCountry: "Canada",
|
||||
settings: {
|
||||
bctenetMaxJobsPerTerm: "25",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "careerjet",
|
||||
importPath: "../extractors/careerjet/manifest",
|
||||
needs: ["CAREERJET_AFFID", "CAREERJET_REFERER", "CAREERJET_USER_IP"],
|
||||
settings: { careerjetMaxJobsPerTerm: "10", searchCities: "United States" },
|
||||
},
|
||||
{
|
||||
id: "eluta",
|
||||
importPath: "../extractors/eluta/manifest",
|
||||
selectedCountry: "Canada",
|
||||
settings: {
|
||||
elutaRssLocations: JSON.stringify(["Toronto, ON"]),
|
||||
elutaMaxJobsPerTerm: "15",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "fourdayweek",
|
||||
importPath: "../extractors/fourdayweek/manifest",
|
||||
settings: { fourdayweekMaxJobsPerTerm: "10" },
|
||||
},
|
||||
{
|
||||
id: "gradcracker",
|
||||
importPath: "../extractors/gradcracker/manifest",
|
||||
selectedCountry: "United Kingdom",
|
||||
settings: { gradcrackerMaxJobsPerTerm: "10" },
|
||||
},
|
||||
{
|
||||
id: "greenhouse",
|
||||
importPath: "../extractors/greenhouse/manifest",
|
||||
@@ -87,14 +129,71 @@ const ALL_TARGETS: Target[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "workday",
|
||||
importPath: "../extractors/workday/manifest",
|
||||
id: "himalayas",
|
||||
importPath: "../extractors/himalayas/manifest",
|
||||
settings: { himalayasMaxJobsPerTerm: "10" },
|
||||
},
|
||||
{
|
||||
id: "hiringcafe",
|
||||
importPath: "../extractors/hiringcafe/manifest",
|
||||
selectedCountry: "United Kingdom",
|
||||
settings: {
|
||||
workdayTenants: JSON.stringify([
|
||||
"https://nvidia.wd5.myworkdayjobs.com/en-US/NVIDIAExternalCareerSite",
|
||||
]),
|
||||
searchCities: "UK",
|
||||
jobspyResultsWanted: "10",
|
||||
workplaceTypes: JSON.stringify(["remote", "hybrid", "onsite"]),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "icims",
|
||||
importPath: "../extractors/icims/manifest",
|
||||
searchTerms: [],
|
||||
settings: {
|
||||
icimsTenants: JSON.stringify(["careers-appliedsystems.icims.com"]),
|
||||
icimsMaxJobsPerTenant: "15",
|
||||
icimsMaxPagesPerSearch: "2",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "jobicy",
|
||||
importPath: "../extractors/jobicy/manifest",
|
||||
settings: { jobicyMaxJobsPerTerm: "10" },
|
||||
},
|
||||
{
|
||||
id: "jobspy",
|
||||
importPath: "../extractors/jobspy/manifest",
|
||||
selectedCountry: "United Kingdom",
|
||||
settings: {
|
||||
searchCities: "UK",
|
||||
jobspyCountryIndeed: "UK",
|
||||
jobspyResultsWanted: "5",
|
||||
workplaceTypes: JSON.stringify(["remote", "hybrid", "onsite"]),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "jooble",
|
||||
importPath: "../extractors/jooble/manifest",
|
||||
needs: ["JOOBLE_API_KEY"],
|
||||
settings: { joobleMaxJobsPerTerm: "10", searchCities: "United States" },
|
||||
},
|
||||
{
|
||||
id: "lever",
|
||||
importPath: "../extractors/lever/manifest",
|
||||
settings: {
|
||||
leverCompanies: JSON.stringify(["palantir", "netflix"]),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "qajobsboard",
|
||||
importPath: "../extractors/qajobsboard/manifest",
|
||||
settings: { qajobsboardMaxJobsPerTerm: "25" },
|
||||
},
|
||||
{
|
||||
id: "reed",
|
||||
importPath: "../extractors/reed/manifest",
|
||||
needs: ["REED_API_KEY"],
|
||||
selectedCountry: "United Kingdom",
|
||||
settings: { reedMaxJobsPerTerm: "10" },
|
||||
},
|
||||
{
|
||||
id: "remoteok",
|
||||
importPath: "../extractors/remoteok/manifest",
|
||||
@@ -106,14 +205,40 @@ const ALL_TARGETS: Target[] = [
|
||||
settings: { remotiveMaxJobsPerTerm: "10" },
|
||||
},
|
||||
{
|
||||
id: "arbeitnow",
|
||||
importPath: "../extractors/arbeitnow/manifest",
|
||||
settings: { arbeitnowMaxJobsPerTerm: "10" },
|
||||
id: "smartrecruiters",
|
||||
importPath: "../extractors/smartrecruiters/manifest",
|
||||
settings: {
|
||||
smartrecruitersCompanies: JSON.stringify(["smartrecruiters"]),
|
||||
smartrecruitersMaxJobsPerCompany: "5",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "himalayas",
|
||||
importPath: "../extractors/himalayas/manifest",
|
||||
settings: { himalayasMaxJobsPerTerm: "10" },
|
||||
id: "startupjobs",
|
||||
importPath: "../extractors/startupjobs/src/manifest",
|
||||
selectedCountry: "United Kingdom",
|
||||
settings: {
|
||||
searchCities: "UK",
|
||||
startupjobsMaxJobsPerTerm: "10",
|
||||
workplaceTypes: JSON.stringify(["remote", "hybrid", "onsite"]),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "themuse",
|
||||
importPath: "../extractors/themuse/manifest",
|
||||
settings: { themuseMaxJobsPerTerm: "10" },
|
||||
},
|
||||
{
|
||||
id: "ukvisajobs",
|
||||
importPath: "../extractors/ukvisajobs/manifest",
|
||||
needs: ["UKVISAJOBS_EMAIL", "UKVISAJOBS_PASSWORD"],
|
||||
selectedCountry: "United Kingdom",
|
||||
settings: { ukvisajobsMaxJobs: "10" },
|
||||
},
|
||||
{
|
||||
id: "usajobs",
|
||||
importPath: "../extractors/usajobs/manifest",
|
||||
needs: ["USAJOBS_API_KEY", "USAJOBS_USER_AGENT"],
|
||||
settings: { usajobsMaxJobsPerTerm: "10" },
|
||||
},
|
||||
{
|
||||
id: "weworkremotely",
|
||||
@@ -121,22 +246,29 @@ const ALL_TARGETS: Target[] = [
|
||||
settings: { weworkremotelyMaxJobsPerTerm: "10" },
|
||||
},
|
||||
{
|
||||
id: "fourdayweek",
|
||||
importPath: "../extractors/fourdayweek/manifest",
|
||||
settings: { fourdayweekMaxJobsPerTerm: "10" },
|
||||
id: "workday",
|
||||
importPath: "../extractors/workday/manifest",
|
||||
settings: {
|
||||
workdayTenants: JSON.stringify([
|
||||
"https://nvidia.wd5.myworkdayjobs.com/en-US/NVIDIAExternalCareerSite",
|
||||
]),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function buildContext(
|
||||
source: string,
|
||||
settings: Record<string, string>,
|
||||
): ExtractorRuntimeContext {
|
||||
function buildContext(args: {
|
||||
source: string;
|
||||
settings: Record<string, string>;
|
||||
searchTerms?: string[];
|
||||
selectedCountry?: string;
|
||||
}): ExtractorRuntimeContext {
|
||||
return {
|
||||
source,
|
||||
selectedSources: [source],
|
||||
settings,
|
||||
searchTerms: ["software engineer"],
|
||||
selectedCountry: "United States",
|
||||
source: args.source,
|
||||
selectedSources: [args.source],
|
||||
settings: args.settings,
|
||||
searchTerms:
|
||||
args.searchTerms !== undefined ? args.searchTerms : ["software engineer"],
|
||||
selectedCountry: args.selectedCountry ?? "United States",
|
||||
getExistingJobUrls: async () => [],
|
||||
shouldCancel: () => false,
|
||||
onProgress: () => {},
|
||||
@@ -151,7 +283,7 @@ async function runOne(target: Target): Promise<void> {
|
||||
const missing = (target.needs ?? []).filter((k) => !process.env[k]);
|
||||
if (missing.length > 0) {
|
||||
console.log(
|
||||
`${pad(target.id, 12)} SKIP missing env: ${missing.join(", ")}`,
|
||||
`${pad(target.id, ID_COL)} SKIP missing env: ${missing.join(", ")}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -161,20 +293,25 @@ async function runOne(target: Target): Promise<void> {
|
||||
mod = await import(target.importPath);
|
||||
} catch (err) {
|
||||
console.log(
|
||||
`${pad(target.id, 12)} FAIL import error: ${(err as Error).message}`,
|
||||
`${pad(target.id, ID_COL)} FAIL import error: ${(err as Error).message}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const manifest = mod.manifest ?? mod.default;
|
||||
if (!manifest) {
|
||||
console.log(`${pad(target.id, 12)} FAIL manifest export missing`);
|
||||
console.log(`${pad(target.id, ID_COL)} FAIL manifest export missing`);
|
||||
return;
|
||||
}
|
||||
|
||||
const started = Date.now();
|
||||
try {
|
||||
const ctx = buildContext(target.id, target.settings ?? {});
|
||||
const ctx = buildContext({
|
||||
source: target.id,
|
||||
settings: target.settings ?? {},
|
||||
searchTerms: target.searchTerms,
|
||||
selectedCountry: target.selectedCountry,
|
||||
});
|
||||
const result = await manifest.run(ctx);
|
||||
const ms = Date.now() - started;
|
||||
const status = result.success ? "OK " : "ERR ";
|
||||
@@ -183,12 +320,12 @@ async function runOne(target: Target): Promise<void> {
|
||||
? ` | first: "${sample.title}" @ ${sample.employer}`
|
||||
: "";
|
||||
console.log(
|
||||
`${pad(target.id, 12)} ${status} jobs=${result.jobs.length} ${ms}ms${result.error ? ` | error: ${result.error}` : ""}${sampleStr}`,
|
||||
`${pad(target.id, ID_COL)} ${status} jobs=${result.jobs.length} ${ms}ms${result.error ? ` | error: ${result.error}` : ""}${sampleStr}`,
|
||||
);
|
||||
} catch (err) {
|
||||
const ms = Date.now() - started;
|
||||
console.log(
|
||||
`${pad(target.id, 12)} CRASH ${ms}ms ${(err as Error).message}`,
|
||||
`${pad(target.id, ID_COL)} CRASH ${ms}ms ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -196,11 +333,13 @@ async function runOne(target: Target): Promise<void> {
|
||||
async function main() {
|
||||
const requested = (process.argv[2] ?? "").trim();
|
||||
const filter = requested
|
||||
? new Set(
|
||||
requested
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
? expandSmokeFilter(
|
||||
new Set(
|
||||
requested
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
const targets = filter
|
||||
|
||||
Reference in New Issue
Block a user