Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions backend/api_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ?")
Expand Down
49 changes: 48 additions & 1 deletion backend/test_deep_audit_regressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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")
Expand Down
13 changes: 9 additions & 4 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2004,7 +2004,7 @@ <h2>No public jobs right now</h2>
} 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');
Expand Down Expand Up @@ -2061,6 +2061,10 @@ <h3 class="job-card-title">${esc(j.title)}</h3>
</div>`;
}

function jobAcceptsApplications(job) {
return ['open', 'reviewing'].includes(job?.status);
}

let jobFilterDebounce = null;
function debounceJobFilter() {
clearTimeout(jobFilterDebounce);
Expand Down Expand Up @@ -2093,7 +2097,8 @@ <h3 class="job-card-title">${esc(j.title)}</h3>
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')}
Expand Down Expand Up @@ -2141,10 +2146,10 @@ <h1 class="detail-title">${esc(job.title)}</h1>
${job.due_by ? `<div class="svc-order-meta-item">${I.clock} <span>Due: ${new Date(job.due_by).toLocaleDateString()}</span></div>` : ''}
<div class="svc-order-meta-item">${I.location} <span>${esc(job.location_type==='remote'?'Remote':(job.location_detail||job.location_type||''))}</span></div>
</div>
${job.status === 'open' && !isOwner
${acceptsApplications && !isOwner
? `<button class="btn btn-primary btn-lg" style="width:100%" onclick="handleJobApply(${id})">${state.user ? 'Apply to This Job' : 'Sign in to Apply'}</button>
<p class="svc-order-note">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.'}</p>`
: job.status !== 'open'
: !acceptsApplications
? `<p style="font-size:var(--text-sm);color:var(--color-text-faint);text-align:center">This job is no longer accepting applications.</p>`
: ''}
</div>
Expand Down
49 changes: 49 additions & 0 deletions frontend/tests/browser-regression.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading