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:
2026-03-06 10:01:53 -08:00
parent a7ce119bde
commit e62756c6ca
6 changed files with 99 additions and 26 deletions

View File

@@ -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;
}