diff --git a/backend/api_core.py b/backend/api_core.py index 0117be6..2ad3f2f 100644 --- a/backend/api_core.py +++ b/backend/api_core.py @@ -8602,10 +8602,16 @@ def _handle_routes(db): budget_type = params.get("budget_type") min_budget = params.get("min_budget") max_budget = params.get("max_budget") - status_filter = params.get("status", "open") + status_filter = params.get("status") + if status_filter: + status_condition = "j.status = ?" + status_values = [status_filter] + else: + status_condition = "j.status IN (?, ?)" + status_values = ["open", "reviewing"] - conditions = ["j.status = ?", f"j.employer_id NOT IN ({public_non_seeded_user_subquery()})"] - values = [status_filter] + seeded_sample_email_values() + conditions = [status_condition, f"j.employer_id NOT IN ({public_non_seeded_user_subquery()})"] + values = status_values + seeded_sample_email_values() if category: conditions.append("j.category = ?") diff --git a/backend/test_deep_audit_regressions.py b/backend/test_deep_audit_regressions.py index f653e2b..09e1e9d 100644 --- a/backend/test_deep_audit_regressions.py +++ b/backend/test_deep_audit_regressions.py @@ -1823,7 +1823,7 @@ def test_jobs_page_highlights_worker_activation_path(self): for snippet in [ "No public jobs right now", "Create a worker profile", - "open job${total === 1 ? '' : 's'} · newest first", + "job${total === 1 ? '' : 's'} accepting applications · newest first", "worker_jobs_apply_cta_click", "worker_job_card_apply_click", "Apply now", @@ -1833,6 +1833,53 @@ def test_jobs_page_highlights_worker_activation_path(self): for contradictory in ["New paid jobs", "View open jobs"]: self.assertNotIn(contradictory, text) + def test_public_jobs_default_includes_every_application_accepting_status(self): + db = self.module.get_db() + try: + db.execute( + "INSERT INTO users (id,email,password_hash,name) VALUES (1,'buyer@real-company.example','x','Real Buyer')" + ) + db.execute( + "INSERT INTO users (id,email,password_hash,name) VALUES (2,'sarah.chen@example.com','x','Seeded Buyer')" + ) + db.execute( + "INSERT INTO users (id,email,password_hash,name) VALUES (3,'worker@real-company.example','x','Real Worker')" + ) + db.execute( + "INSERT INTO sessions (user_id,token,expires_at) VALUES (3,'tok-reviewing-discovery',datetime('now','+1 day'))" + ) + for job_id, employer_id, title, status in [ + (1, 1, "Fresh public job", "open"), + (2, 1, "Public job under review", "open"), + (3, 1, "Already hired job", "hired"), + (4, 2, "Seeded sample job", "open"), + ]: + db.execute( + """INSERT INTO jobs + (id,employer_id,title,description,category,budget_type,budget_amount,status) + VALUES (?,?,?,?,?,'fixed',25,?)""", + [job_id, employer_id, title, "Bounded work", "testing", status], + ) + db.commit() + finally: + db.close() + + status, application = self._request_api( + "POST", + "/jobs/2/apply", + {"cover_message": "I can complete this bounded review."}, + "tok-reviewing-discovery", + ) + self.assertEqual(status, 201, application) + + status, response = self._request_api("GET", "/jobs") + self.assertEqual(status, 200, response) + self.assertEqual(response["total"], 2) + self.assertEqual( + {job["id"]: job["status"] for job in response["jobs"]}, + {1: "open", 2: "reviewing"}, + ) + def test_public_homepage_visual_cleanup_invariants(self): text = (REPO_ROOT / "frontend/index.html").read_text(encoding="utf-8", errors="ignore") diff --git a/frontend/index.html b/frontend/index.html index 2751b16..743a595 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2004,7 +2004,7 @@

No public jobs right now

} else { if (summary) { summary.hidden = false; - summary.textContent = `${total} open job${total === 1 ? '' : 's'} · newest first`; + summary.textContent = `${total} job${total === 1 ? '' : 's'} accepting applications · newest first`; } if (filtersEl) filtersEl.hidden = false; if (layout) layout.classList.remove('is-empty'); @@ -2061,6 +2061,10 @@

${esc(j.title)}

`; } +function jobAcceptsApplications(job) { + return ['open', 'reviewing'].includes(job?.status); +} + let jobFilterDebounce = null; function debounceJobFilter() { clearTimeout(jobFilterDebounce); @@ -2093,7 +2097,8 @@

${esc(j.title)}

const job = await api(`/jobs/${id}`); const skills = safeParseJSON(job.required_skills, []); const isOwner = state.user && String(state.user.id) === String(job.employer_id); - const shouldAutoApply = getQuery().get('apply') === '1' && state.user && job.status === 'open' && !isOwner; + const acceptsApplications = jobAcceptsApplications(job); + const shouldAutoApply = getQuery().get('apply') === '1' && state.user && acceptsApplications && !isOwner; document.getElementById('app').innerHTML = ` ${renderPublicNav('jobs')} @@ -2141,10 +2146,10 @@

${esc(job.title)}

${job.due_by ? `
${I.clock} Due: ${new Date(job.due_by).toLocaleDateString()}
` : ''}
${I.location} ${esc(job.location_type==='remote'?'Remote':(job.location_detail||job.location_type||''))}
- ${job.status === 'open' && !isOwner + ${acceptsApplications && !isOwner ? `

Free to apply. No payment needed until you're hired. ${state.user ? 'Application opens in this page.' : 'We will bring you back to this job after sign-in.'}

` - : job.status !== 'open' + : !acceptsApplications ? `

This job is no longer accepting applications.

` : ''} diff --git a/frontend/tests/browser-regression.spec.js b/frontend/tests/browser-regression.spec.js index 1cc8b65..c9d38ff 100644 --- a/frontend/tests/browser-regression.spec.js +++ b/frontend/tests/browser-regression.spec.js @@ -1332,6 +1332,55 @@ test.describe('GoHireHumans public/browser regression suite', () => { await expect(page.getByText('View open jobs', { exact: true })).toHaveCount(0); }); + test('reviewing jobs remain discoverable and applicable while applications are accepted', async ({ page }) => { + await setupDeterministicLocalPage(page); + const reviewingJob = { + id: 42, + employer_id: 8, + employer_name: 'GoHireHumans Operations', + title: 'Review a live workflow', + description: 'Return screenshots and a prioritized issue list.', + category: 'testing', + status: 'reviewing', + budget_type: 'fixed', + budget_amount: 35, + location_type: 'remote', + created_at: '2026-07-21T00:00:00Z', + application_count: 1 + }; + await page.route('https://gohirehumans-production.up.railway.app/jobs**', route => { + const url = new URL(route.request().url()); + if (url.pathname.endsWith('/jobs/42')) { + return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(reviewingJob) }); + } + if (url.pathname.endsWith('/jobs/43')) { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ ...reviewingJob, id: 43, title: 'Already hired workflow', status: 'hired' }) + }); + } + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ jobs: [reviewingJob], total: 1 }) + }); + }); + + await page.goto('/#/jobs', { waitUntil: 'domcontentloaded' }); + await expect(page.getByText('Review a live workflow')).toBeVisible(); + await expect(page.locator('#jobs-summary')).toHaveText('1 job accepting applications · newest first'); + await page.getByRole('button', { name: 'Apply now' }).click(); + await expect(page).toHaveURL(/#\/jobs\/42$/); + await expect(page.getByRole('button', { name: 'Sign in to Apply' })).toBeVisible(); + await expect(page.locator('body')).not.toContainText('This job is no longer accepting applications.'); + + await page.goto('/#/jobs/43', { waitUntil: 'domcontentloaded' }); + await expect(page.getByText('Already hired workflow')).toBeVisible(); + await expect(page.getByText('This job is no longer accepting applications.')).toBeVisible(); + await expect(page.getByRole('button', { name: /Apply/ })).toHaveCount(0); + }); + test('filtered-empty marketplace states preserve truthful recovery paths', async ({ page }) => { await setupDeterministicLocalPage(page); await page.route('https://gohirehumans-production.up.railway.app/jobs**', route => route.fulfill({