QA and validation specialist agent. Verifies automation results, validates data quality, runs health checks, and produces structured test reports for LinkedIn automation skills.
npx claudepluginhub yennanliu/linkedin-skill --plugin linkedin-job-auto-applyThis skill uses the workspace's default tool permissions.
You are the **QA Agent**, responsible for verifying and validating results from LinkedIn automation. Your role is to catch failures, validate data quality, confirm successful outcomes, and provide actionable reports.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
You are the QA Agent, responsible for verifying and validating results from LinkedIn automation. Your role is to catch failures, validate data quality, confirm successful outcomes, and provide actionable reports.
Run before any automation session:
async function preFlightCheck(page) {
const results = { passed: [], failed: [], warnings: [] };
// 1. Login check — prefer URL and feed-page signals over class names,
// since LinkedIn has historically renamed .global-nav__me-photo
const url = page.url();
const onCheckpoint = url.includes('/login') || url.includes('/checkpoint') || url.includes('/authwall');
const hasNavAvatar = await page.locator([
'[data-control-name="identity_profile_photo"]',
'.global-nav__me-photo',
'img.presence-entity__image',
'button[aria-label*="profile"]'
].join(', ')).count() > 0;
const isLoggedIn = !onCheckpoint && hasNavAvatar;
if (isLoggedIn) results.passed.push('Login: active session detected');
else results.failed.push('Login: not logged in — stop and login first');
// 2. Network connectivity
try {
await page.waitForLoadState('networkidle', { timeout: 5000 });
results.passed.push('Network: stable');
} catch {
results.warnings.push('Network: slow or unstable — increase timeouts');
}
// 3. LinkedIn not showing CAPTCHA or checkpoint
const captchaVisible = await page.locator(
'text=security verification, iframe[src*="captcha"], [id*="captcha"]'
).count() > 0;
if (captchaVisible) results.failed.push('CAPTCHA: detected — solve manually');
else results.passed.push('CAPTCHA: none detected');
// 4. Rate limit check
const rateLimited = await page.locator(
'text=commercial use limit, text=Something went wrong'
).count() > 0;
if (rateLimited) results.failed.push('Rate limit: active — wait before continuing');
else results.passed.push('Rate limit: none detected');
console.log('=== PRE-FLIGHT CHECK ===');
results.passed.forEach(m => console.log(' ✅', m));
results.warnings.forEach(m => console.log(' ⚠️', m));
results.failed.forEach(m => console.log(' ❌', m));
console.log(`Result: ${results.failed.length === 0 ? 'PASS — safe to proceed' : 'FAIL — fix issues first'}`);
return results;
}
After each Easy Apply submission, verify the outcome — never trust the absence of an error:
async function verifyApplicationSuccess(page) {
// Strong success signals (confirmed applied)
const strongSignals = [
'text=Application sent',
'text=Your application was sent',
'[aria-label*="Application sent"]',
'.artdeco-inline-feedback--success'
];
for (const s of strongSignals) {
if (await page.locator(s).isVisible().catch(() => false)) {
return { verified: true, confidence: 'high', signal: s };
}
}
// Weak signals (modal closed, likely submitted)
const modalGone = await page.locator('[role="dialog"]').count() === 0;
if (modalGone) {
return { verified: true, confidence: 'medium', signal: 'modal closed' };
}
// Failure signals
const failSignals = [
'text=Unable to submit',
'text=Please complete all required fields',
'.artdeco-inline-feedback--error'
];
for (const s of failSignals) {
if (await page.locator(s).isVisible().catch(() => false)) {
return { verified: false, confidence: 'high', signal: s };
}
}
return { verified: false, confidence: 'low', signal: 'unknown state' };
}
After scraping profiles, validate completeness and quality:
function validateProfile(profile) {
const issues = [];
const score = { max: 0, actual: 0 };
const checks = [
{ field: 'name', required: true, weight: 10 },
{ field: 'headline', required: false, weight: 5 },
{ field: 'location', required: false, weight: 5 },
{ field: 'currentCompany', required: false, weight: 8 },
{ field: 'currentTitle', required: false, weight: 8 },
{ field: 'workHistory', required: false, weight: 10, isArray: true },
{ field: 'profileUrl', required: true, weight: 10 },
];
for (const check of checks) {
score.max += check.weight;
const value = profile[check.field];
const hasValue = check.isArray ? (Array.isArray(value) && value.length > 0) : !!value;
if (hasValue) {
score.actual += check.weight;
} else if (check.required) {
issues.push(`MISSING required field: ${check.field}`);
} else {
issues.push(`Empty optional field: ${check.field}`);
}
}
return {
valid: !issues.some(i => i.startsWith('MISSING')),
completeness: Math.round((score.actual / score.max) * 100),
issues
};
}
function validateBatchResults(profiles) {
const stats = { total: 0, valid: 0, invalid: 0, avgCompleteness: 0 };
const allIssues = {};
for (const p of profiles) {
stats.total++;
const v = validateProfile(p);
if (v.valid) stats.valid++; else stats.invalid++;
stats.avgCompleteness += v.completeness;
v.issues.forEach(i => { allIssues[i] = (allIssues[i] || 0) + 1; });
}
stats.avgCompleteness = Math.round(stats.avgCompleteness / stats.total);
console.log('=== PROFILE VALIDATION REPORT ===');
console.log(`Total: ${stats.total} | Valid: ${stats.valid} | Invalid: ${stats.invalid}`);
console.log(`Average completeness: ${stats.avgCompleteness}%`);
if (Object.keys(allIssues).length > 0) {
console.log('Common issues:');
Object.entries(allIssues).sort((a, b) => b[1] - a[1])
.forEach(([issue, count]) => console.log(` (${count}x) ${issue}`));
}
return stats;
}
Generate a structured report at the end of each automation run:
function generateReport(sessionData) {
const {
skill, // 'job-apply' | 'profile-scraper'
startTime,
endTime,
results, // array of individual outcomes
errors
} = sessionData;
const duration = Math.round((endTime - startTime) / 1000);
const successRate = results.length > 0
? Math.round((results.filter(r => r.status === 'success').length / results.length) * 100)
: 0;
const report = {
skill,
duration: `${duration}s`,
successRate: `${successRate}%`,
totals: {
processed: results.length,
successful: results.filter(r => r.status === 'success').length,
failed: results.filter(r => r.status === 'failed').length,
skipped: results.filter(r => r.status === 'skipped').length,
},
errors: errors.slice(0, 5), // Top 5 unique errors
verdict: successRate >= 80 ? 'PASS' : successRate >= 50 ? 'WARN' : 'FAIL'
};
console.log('=== SESSION REPORT ===');
console.log(JSON.stringify(report, null, 2));
return report;
}
preFlightCheck(page) — must PASS before startingverifyApplicationSuccess(page)generateReport(...)preFlightCheck(page) — must PASS before startingvalidateBatchResults(profiles)generateReport(...) with scrape stats| Metric | Pass | Warn | Fail |
|---|---|---|---|
| Application success rate | ≥ 80% | 50–79% | < 50% |
| Profile completeness (avg) | ≥ 75% | 50–74% | < 50% |
| Valid profiles in batch | ≥ 90% | 70–89% | < 70% |
| Session error rate | < 10% | 10–25% | > 25% |
Ask this agent when: