fix: robustness improvements — atomic writes, timeouts, shell injection, validation errors
- Atomic JSON writes (write-to-tmp + rename) prevent queue/log corruption
- Per-job (3min) and overall run (45min) timeouts prevent hangs
- execFileSync in ai_answer.mjs prevents shell injection with resume paths
- Validation error detection after form fill in Easy Apply modal
- Config-driven enabled_apply_types (from settings.json)
- isRequired() detects required/aria-required/label * patterns
- getLabel() strips trailing * from required field labels
- Actionable logging on failures ("Action: ..." messages)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -83,7 +83,9 @@ export async function apply(page, job, formFiller) {
|
||||
Array.from(document.querySelectorAll('[aria-label*="Easy Apply"], [aria-label*="Apply"]'))
|
||||
.map(el => ({ tag: el.tagName, aria: el.getAttribute('aria-label'), visible: el.offsetParent !== null }))
|
||||
).catch(() => []);
|
||||
console.log(` ℹ️ No Easy Apply element found. Apply-related elements: ${JSON.stringify(applyEls)}`);
|
||||
console.log(` ℹ️ No Easy Apply button found. Page URL: ${page.url()}`);
|
||||
console.log(` ℹ️ Apply-related elements on page: ${JSON.stringify(applyEls)}`);
|
||||
console.log(` Action: job may have been removed, filled, or changed to external apply`);
|
||||
return { status: 'skipped_easy_apply_unsupported', meta };
|
||||
}
|
||||
|
||||
@@ -97,7 +99,11 @@ export async function apply(page, job, formFiller) {
|
||||
// Click Easy Apply and wait for modal to appear
|
||||
await page.click(LINKEDIN_APPLY_BUTTON_SELECTOR, { timeout: APPLY_CLICK_TIMEOUT }).catch(() => {});
|
||||
const modal = await page.waitForSelector(LINKEDIN_EASY_APPLY_MODAL_SELECTOR, { timeout: 8000 }).catch(() => null);
|
||||
if (!modal) return { status: 'no_modal', meta };
|
||||
if (!modal) {
|
||||
console.log(` ❌ Modal did not open after clicking Easy Apply`);
|
||||
console.log(` Action: LinkedIn may have changed the modal structure or login expired`);
|
||||
return { status: 'no_modal', meta };
|
||||
}
|
||||
|
||||
const MODAL = LINKEDIN_EASY_APPLY_MODAL_SELECTOR;
|
||||
|
||||
@@ -146,6 +152,22 @@ export async function apply(page, job, formFiller) {
|
||||
|
||||
await page.waitForTimeout(MODAL_STEP_WAIT);
|
||||
|
||||
// Check for validation errors after form fill — if LinkedIn shows errors,
|
||||
// the form won't advance. Re-check errors AFTER fill since fill may have resolved them.
|
||||
const postFillErrors = await page.evaluate((sel) => {
|
||||
const modal = document.querySelector(sel);
|
||||
if (!modal) return [];
|
||||
return Array.from(modal.querySelectorAll('[class*="error"], [aria-invalid="true"], .artdeco-inline-feedback--error'))
|
||||
.map(e => e.textContent?.trim().slice(0, 80)).filter(Boolean);
|
||||
}, MODAL).catch(() => []);
|
||||
|
||||
if (postFillErrors.length > 0) {
|
||||
console.log(` [step ${step}] ❌ Validation errors after fill: ${JSON.stringify(postFillErrors)}`);
|
||||
console.log(` Action: check answers.json or profile.json for missing/wrong answers`);
|
||||
await dismissModal(page, MODAL);
|
||||
return { status: 'incomplete', meta, validation_errors: postFillErrors };
|
||||
}
|
||||
|
||||
// --- Button check order: Next → Review → Submit ---
|
||||
// Check Next first — only fall through to Submit when there's no forward navigation.
|
||||
// This prevents accidentally clicking a Submit-like element on early modal steps.
|
||||
@@ -195,7 +217,8 @@ export async function apply(page, job, formFiller) {
|
||||
return { status: 'stuck', meta };
|
||||
}
|
||||
|
||||
console.log(` [step ${step}] no Next/Review/Submit found — breaking`);
|
||||
console.log(` [step ${step}] ❌ No Next/Review/Submit button found in modal`);
|
||||
console.log(` Action: LinkedIn may have changed button text/structure. Check button snapshot above.`);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user