diff --git a/.assets-revision b/.assets-revision index 94075662..fabc7ecf 100644 --- a/.assets-revision +++ b/.assets-revision @@ -5,4 +5,4 @@ # is a git revision (branch name like `main`, a tag, or a specific commit # sha). Override at runtime with the ASSETS_REVISION env var. repo: ChilleD/WebHarbor -revision: 070123d74c01a8b29808201be85462fd7d0ec3c4 +revision: 65c479f894763f64c6073e0d180ebf542d1d2c02 diff --git a/.dockerignore b/.dockerignore index a921bf15..2e0a5b85 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,6 +8,13 @@ sites/*/instance/ # Don't ship — runtime doesn't need scrape intermediate (data is in instance_seed/*.db). sites/*/scraped_data/ +# Don't ship — per-site local dev helpers (asset harvest, smoke tests, leak audits). +sites/*/scripts_dev/ +sites/*/tests/ +sites/*/verify/tests/ +agent_demo/runs/ +.venv/ + # Don't ship — bytecode / venvs. sites/*/__pycache__/ **/__pycache__/ diff --git a/.gitignore b/.gitignore index e7899232..4f14a819 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ sites/*/static/external_cache/ # ============================================================= # scrape pipeline intermediate; runtime data lives in instance_seed/*.db sites/*/scraped_data/ +/scraped_data/ # rebuilt at every container boot from instance_seed/ sites/*/instance/ sites/*/venv/ @@ -95,3 +96,7 @@ secrets.json # Agent demo results # ============================================================= agent_demo/runs/ + +# walmart_careers verifier validation runs (run signatures + DB snapshots): never committed. +sites/*/scripts_dev/runs/ +sites/*/scripts_dev/**/*.db diff --git a/AGENTS.md b/AGENTS.md index aab19b7a..22696c27 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,17 +48,17 @@ Inside the image, sites live at `/opt/WebSyn//`. The path predates the ren # fresh clone ./scripts/fetch_assets.sh # pulls assets from HF ./scripts/build.sh # docker build -t webharbor:dev . -docker run -d -p 8101:8101 -p 40000-40022:40000-40022 webharbor:dev +docker run -d -p 8101:8101 -p 40000-40023:40000-40023 webharbor:dev ``` Or use the published image directly: ```bash -docker run -d -p 8101:8101 -p 40000-40022:40000-40022 \ +docker run -d -p 8101:8101 -p 40000-40023:40000-40023 \ battalion7244/webharbor:latest ``` -Sites are on `40000`-`40022` in the order declared by `SITES=( ... )` in `websyn_start.sh`. Control plane: +Sites are on `40000`-`40023` in the order declared by `SITES=( ... )` in `websyn_start.sh`. Control plane: | Method | Path | Purpose | |--------|---------------------|-------------------------------------------| @@ -136,13 +136,13 @@ python3 -m py_compile sites//app.py # 3. run on alt ports (don't collide with anything you already have running) docker run -d --rm --name wh-test \ - -p 8201:8101 -p 41000-41022:40000-40022 webharbor:dev + -p 8201:8101 -p 41000-41023:40000-40023 webharbor:dev # 4. control plane healthy, all sites alive curl -s http://localhost:8201/health | python3 -m json.tool | head # 5. every site renders 200 -for p in $(seq 41000 41022); do +for p in $(seq 41000 41023); do curl -so /dev/null -w "$p:%{http_code}\n" http://localhost:$p/ done diff --git a/CLAUDE.md b/CLAUDE.md index 50c5f0db..12e3cb0f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,4 +16,4 @@ The full agent guide is loaded above via `@AGENTS.md`. The notes below apply onl ## Existing containers -If a container is already running on `:8101` / `:40000-40022`, treat it as the user's working environment — don't `docker stop` or `docker rm` it without explicit confirmation. Spin up your test container under a different name on alt ports (`:8201`, `:41000-41022`). +If a container is already running on `:8101` / `:40000-40023`, treat it as the user's working environment — don't `docker stop` or `docker rm` it without explicit confirmation. Spin up your test container under a different name on alt ports (`:8201`, `:41000-41023`). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 20907615..5f36384c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ git clone https://github.com//webharbor && cd webharbor ./scripts/fetch_assets.sh # pull current assets ./scripts/new_site.py mywebsite # OR edit an existing site ./scripts/build.sh && docker run -d --rm \ - -p 8101:8101 -p 40000-40022:40000-40022 webharbor:dev + -p 8101:8101 -p 40000-40023:40000-40023 webharbor:dev # iterate locally... ./scripts/extract_assets.sh ../webharbor-static-pr/ # split assets out diff --git a/Dockerfile b/Dockerfile index d3fa9c8c..86c17615 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # WebHarbor — slim, self-contained image. -# 23 Flask mirror sites + control plane on :8101. +# 24 Flask mirror sites + control plane on :8101. FROM python:3.12-slim-bookworm @@ -44,6 +44,12 @@ RUN python3 /opt/check_asset_inventory.py /opt/WebSyn/compass RUN cd /opt/WebSyn/compass && rm -rf instance instance_seed && \ PYTHONHASHSEED=0 python migrate_seed.py && rm -rf instance +# Walmart Careers validates source-backed media and rebuilds its deterministic SQLite seed from tracked source data. +RUN python3 /opt/check_asset_inventory.py /opt/WebSyn/walmart_careers && \ + python3 /opt/WebSyn/walmart_careers/check_tracked_assets.py +RUN cd /opt/WebSyn/walmart_careers && rm -rf instance instance_seed && \ + PYTHONHASHSEED=0 python seed_data.py && rm -rf instance + COPY websyn_start.sh /opt/websyn_start.sh COPY control_server.py /opt/control_server.py COPY site_runner.py /opt/site_runner.py @@ -66,6 +72,6 @@ os.makedirs('instance_seed', exist_ok=True); \ shutil.copy2('instance/rotten_tomatoes.db', 'instance_seed/rotten_tomatoes.db'); \ print('Rotten Tomatoes seed DB generated at build time.')" && rm -rf /opt/WebSyn/rotten_tomatoes/instance -EXPOSE 8101 40000-40022 +EXPOSE 8101 40000-40023 CMD ["/opt/websyn_start.sh"] diff --git a/README.md b/README.md index 610c7e09..a05d266a 100644 --- a/README.md +++ b/README.md @@ -36,17 +36,17 @@ WebHarbor takes a different approach. We leverage coding agent (e.g., Claude Cod - **Deep features unlocked** — carts, checkouts, accounts, all fully testable - **Evolving** — harder tasks drive richer mirrors; the environment grows with agents - **RL-ready** — sub-second database resets between rollouts -- **Community-driven** — 23 sites today, scaling to 100+ together +- **Community-driven** — 24 sites today, scaling to 100+ together ## 🚀 Quickstart One command to run all web environments: ```bash -docker run -p 8101:8101 -p 40000-40022:40000-40022 battalion7244/webharbor:latest +docker run -p 8101:8101 -p 40000-40023:40000-40023 battalion7244/webharbor:latest ``` -Then point your agent at `http://localhost:40000` through `http://localhost:40022` to explore 23 local mirrors of WebVoyager sites: `Allrecipes, Amazon, Apple, ArXiv, BBC News, Booking, GitHub, Google Flights, Google Maps, Google Search, Hugging Face, Wolfram Alpha, Cambridge Dictionary, Coursera, ESPN, Merriam-Webster, IKEA, Phys.org, Target, TED, Ohio State University, Rotten Tomatoes, and Compass`. +Then point your agent at `http://localhost:40000` through `http://localhost:40023` to explore 24 local mirrors of WebVoyager sites: `Allrecipes, Amazon, Apple, ArXiv, BBC News, Booking, GitHub, Google Flights, Google Maps, Google Search, Hugging Face, Wolfram Alpha, Cambridge Dictionary, Coursera, ESPN, Merriam-Webster, IKEA, Phys.org, Target, TED, Ohio State University, Rotten Tomatoes, Compass, and Walmart Careers`. For sub-second reset between rollouts, expose the control plane and call `/reset/`: diff --git a/agent_demo/README.md b/agent_demo/README.md index cd664e8a..44c25e60 100644 --- a/agent_demo/README.md +++ b/agent_demo/README.md @@ -19,7 +19,7 @@ export OPENAI_BASE_URL=https://api.openai.com/v1 # or your Azure / vLLM endpoi ## Run a task -WebHarbor must already be running locally (`docker run -p 8101:8101 -p 40000-40022:40000-40022 battalion7244/webharbor:latest`). +WebHarbor must already be running locally (`docker run -p 8101:8101 -p 40000-40023:40000-40023 battalion7244/webharbor:latest`). Run a single task from a site's `tasks.jsonl`: diff --git a/control_server.py b/control_server.py index 5602ce02..7df0d9ee 100644 --- a/control_server.py +++ b/control_server.py @@ -26,7 +26,7 @@ 'allrecipes', 'amazon', 'apple', 'arxiv', 'bbc_news', 'booking', 'github', 'google_flights', 'google_map', 'google_search', 'huggingface', 'wolfram_alpha', 'cambridge_dictionary', - 'coursera', 'espn', 'merriam_webster', 'ikea', 'phys_org', 'target', 'ted', 'osu', 'rotten_tomatoes', 'compass', + 'coursera', 'espn', 'merriam_webster', 'ikea', 'phys_org', 'target', 'ted', 'osu', 'rotten_tomatoes', 'compass', 'walmart_careers', ] BASE_PORT = 40000 WEBSYN_DIR = '/opt/WebSyn' @@ -48,6 +48,7 @@ # for those. _site_procs: dict = {} _site_procs_lock = threading.Lock() +_reap_lock = threading.Lock() # We tried graceful SIGTERM. Werkzeug's threaded serve_forever() doesn't # honor it. Since /reset wipes instance/ next anyway, in-flight transactions @@ -91,6 +92,18 @@ def is_alive(pid) -> bool: return False +def reap_exited_children() -> None: + """Reap every exited direct child, including re-parented Flask workers.""" + with _reap_lock: + while True: + try: + pid, _status = os.waitpid(-1, os.WNOHANG) + except ChildProcessError: + return + if pid <= 0: + return + + def kill_site(site: str, reap_grace: float = REAP_GRACE_SECS): pid = read_pid(site) if not pid: @@ -101,11 +114,8 @@ def kill_site(site: str, reap_grace: float = REAP_GRACE_SECS): os.killpg(pid, signal.SIGKILL) except ProcessLookupError: pass - # If we own a Popen for this supervisor, wait()+reap so it doesn't - # linger as a zombie. (Supervisors started at boot via websyn_start.sh - # aren't tracked here; we still adopted them as children via container - # init, but Python won't reap them — they stay zombies until container - # exit. That's harmless: is_alive() correctly reports them as dead.) + # Reap supervisors created through Popen and boot-time supervisors that + # became direct children when websyn_start.sh exec'd this control process. with _site_procs_lock: proc = _site_procs.pop(site, None) if proc is not None: @@ -113,12 +123,16 @@ def kill_site(site: str, reap_grace: float = REAP_GRACE_SECS): proc.wait(timeout=reap_grace) except subprocess.TimeoutExpired: pass + reap_exited_children() # Belt-and-suspenders: confirm the supervisor is actually dead before # returning, even when we don't own the Popen. is_alive() looks at # /proc state and returns False for zombies, so this loop exits in ms. deadline = time.time() + reap_grace while time.time() < deadline: if not is_alive(pid): + for _ in range(10): + reap_exited_children() + time.sleep(0.01) return time.sleep(0.01) raise RuntimeError(f'failed to stop {site} process group {pid}') @@ -187,6 +201,8 @@ def restart_one(site: str) -> dict: @app.route('/health') def health(): + reap_exited_children() + def status(site): pid = read_pid(site) alive = is_alive(pid) diff --git a/review-reports/PR-86-FINAL-AUDIT.md b/review-reports/PR-86-FINAL-AUDIT.md new file mode 100644 index 00000000..5d77e9d0 --- /dev/null +++ b/review-reports/PR-86-FINAL-AUDIT.md @@ -0,0 +1,64 @@ +# PR 86 Final Audit + +## Scope + +This audit covers `aiming-lab/WebHarbor#86`, whose reviewed head was `a2649095bbef7d0fbce011d4510e6ee1cbd08d68`. The PR was reviewed against upstream `main` at `129a274230070abb90b6d4ec209d81a911754ec9`. Eight independent review agents examined security and state, application and data behavior, task ground truth, verifier resistance, UI and responsive behavior, repository and container integration, test evidence, and asset provenance. + +The original PR was conflicting with current `main`. Current `main` already assigned internal port `40022` to Compass, so this integration preserves Compass and registers Walmart Careers as the twenty-fourth site on internal port `40023`. + +## Agent Findings and Resolutions + +| Agent | Review area | Verified findings | Resolution | +| --- | --- | --- | --- | +| 1 | Security and state | `sites/walmart_careers/app.py` used a fixed Flask secret, application contact data was stored in the client session, input boundaries and transaction failure handling were incomplete, and SQLite foreign-key enforcement was connection-dependent. | Added environment/random secret handling, cookie and request-size controls, CSRF protection, strict field limits, per-connection foreign keys, transaction rollback, race handling, and server-side one-time application drafts with ownership and replay checks. | +| 2 | Application and data | Unknown locations could degrade into an unfiltered result set, query semantics were permissive, and profile values could be silently truncated. | Unknown or unsupported locations now return zero results; duplicate and invalid query parameters, facets, sorting, pagination, radii, tabs, and excessive search tokens are rejected; field validation rejects overlong input rather than truncating it. | +| 3 | Tasks and ground truth | Several task verifiers omitted required filter constraints, one task description was ambiguous, and the signed-in email behavior conflicted with one rubric. | Clarified affected task text, enforced every required filter, and derived task targets and candidate sets from the supplied initial database through `verify/ground_truth.py`. | +| 4 | Verifier resistance | Screenshot bytes were not decoded, trajectory URLs could be fabricated, protected schema/catalog rows were not comprehensively checked, stateful verifiers allowed unrelated writes, and several numeric/address checks accepted weak context. | Added complete PNG decoding, loopback origin and port checks, exact nine-table schema hashing, protected-table checks, exact allowed row deltas, required workflow ordering, candidate-detail coverage, and strict contextual matching for addresses, identifiers, confirmation numbers, time, counts, and experience. | +| 5 | UI and responsive behavior | The original implementation overflowed at 320 px, exposed misleading inert controls, and had menu, dialog, table, focus, heading, and reduced-motion defects. | Reworked responsive layout and overlays, added scroll containment and focus treatment, corrected headings and form semantics, honored reduced-motion preferences, and removed non-functional play, pause, carousel, and share controls. | +| 6 | Repository, assets, and Docker | Compass already occupied `40022`; the original asset proposal was not based on current asset `main`; the repository and container registries still assumed 23 sites. | Merged current `main`, retained Compass on `40022`, placed Walmart on `40023`, updated all 24-site registries and port ranges, and pinned assets to revision `65c479f894763f64c6073e0d180ebf542d1d2c02`. | +| 7 | Tests and release evidence | The original tests were predominantly hand-built verifier fixtures and did not cover Flask routes, container behavior, assets, complete schema integrity, or all integration contracts. | Added route, security, state, seed, asset, task-ground-truth, verifier-adversarial, and 24-site integration tests; validated the release image with real browser workflows and control-plane operations. | +| 8 | Asset provenance and classification | Media, icons, and fonts lacked complete machine-verifiable inventories; synthetic benchmark data was not explicitly classified; old documentation and dependency declarations were incomplete. | Added checksummed inventories for 35 downloaded media assets and 27 tracked assets, added provenance documentation, classified jobs/users/applications/tasks as synthetic benchmark data, removed three unused or unverified tracked assets, and pinned runtime dependencies. | + +## Additional Verified Fixes + +- `sites/walmart_careers/seed_data.py` now constructs a deterministic database in a temporary path and atomically publishes it with seed version `walmart-careers-v2`. +- Seed startup validation protects immutable catalog counts and required benchmark users while permitting legitimate runtime registrations, saved jobs, and applications. Restart preserves runtime state; reset restores the seed. +- `control_server.py` reaps exited direct children and re-parented Flask workers, preventing reset/restart zombie accumulation under PID 1. +- Health checks validate the immutable catalog and required benchmark identities without treating valid runtime rows as seed corruption. +- The release image builds seed databases from tracked deterministic source and does not package tests, verifier test fixtures, development scripts, or answer-bearing browser evidence. +- The Walmart media archive contains 35 files, is 11,458,196 bytes, and has SHA-256 `b84a0072c603225f29e21edfd8f8038fe02a0261a38a60f8e70ed871ea7ce77a`. + +## Validation Results + +| Validation | Result | +| --- | --- | +| Walmart application, integration, seed, asset, ground-truth, and verifier suites | 297 passed; 16 unittest subtests passed | +| Compass regression suite | 229 passed | +| Rotten Tomatoes regression suite | 66 passed; 4,143 unittest subtests passed | +| OSU regression suite | 25 passed; 264 unittest subtests passed | +| TED regression suite | 27 passed; 291 unittest subtests passed | +| Ruff fatal-error/undefined-name checks | Passed | +| Python byte compilation | Passed | +| Shell syntax and Git whitespace checks | Passed | +| Walmart media inventory | 35 files verified | +| Walmart tracked asset inventory | 27 files verified | +| Compass media inventory in Docker build | 2,907 files verified | +| Release Docker image | `sha256:13df807773246725ab8b1999c03d87b5efb1be0bab8eaaf625bf14d1c31a68f7` | +| Browser execution of all Walmart tasks | 20/20 passed; 106 browser steps; deterministic verifier passed each task | +| Responsive/browser matrix | 21 routes × 5 viewport widths = 105 checks; zero overflow, clipping, broken images, unlabeled controls, page errors, or remote requests | +| Narrow-screen overlays | Four navigation menus and three result popovers remained within the viewport | +| No-JavaScript static behavior | Passed; no misleading media controls remained | +| Release container control plane | 24/24 sites ready; all 24 roots returned successful responses | +| Release reset-all | Passed; all 24 runtime seed trees matched their seed trees | +| Walmart restart and reset | Valid runtime state survived restart; reset restored byte-identical seed state | +| Ten consecutive Walmart resets | Passed; PID 1 file descriptors remained 4 → 4 and zombie count remained 0 → 0 | + +## Post-review Navigation Correction + +A reported header-navigation defect was reproduced at the 1080 × 397 CSS-pixel viewport represented by the supplied screenshot. Opening Career areas, Brands, and Resources left all three native `details` elements open; clicking outside or pressing Escape did not close them. The live Walmart Careers header was independently checked and kept only one primary menu expanded while closing it on outside pointer interaction and Escape. + +`static/js/navigation.js` now provides mutually exclusive header menus, same-trigger close, outside-pointer close, focus-leave close, Escape close with focus restoration, and mutual exclusion with the account menu. Packaged Chromium checks passed at 1080 × 397 and 320 × 480, the 20-task browser/verifier suite remained 20/20 with 106 steps, and the 105-case responsive matrix remained free of overflow, clipping, broken images, unlabeled controls, page errors, and remote requests. + +## GitHub Write Policy + +No GitHub comment or review was submitted during this audit. The permitted GitHub write operation is limited to pushing the reviewed commit to the existing PR head branch. diff --git a/scripts/check_assets.sh b/scripts/check_assets.sh index 21a13258..d74cb589 100755 --- a/scripts/check_assets.sh +++ b/scripts/check_assets.sh @@ -34,7 +34,7 @@ for site in sites/*/; do warnings=$((warnings + 1)) fi done - if [[ -f "sites/$s/asset_inventory.json" && -d "sites/$s/static/images" && -d "sites/$s/static/external_cache" ]]; then + if [[ -f "sites/$s/asset_inventory.json" ]]; then python3 scripts/check_asset_inventory.py "sites/$s" fi done diff --git a/sites/rotten_tomatoes/tests/test_environment_quality.py b/sites/rotten_tomatoes/tests/test_environment_quality.py index a8ee2c4a..806af708 100644 --- a/sites/rotten_tomatoes/tests/test_environment_quality.py +++ b/sites/rotten_tomatoes/tests/test_environment_quality.py @@ -88,9 +88,9 @@ def test_task_manifest_and_registry(self): self.assertEqual(row['web'], 'http://localhost:40021/') self.assertTrue((ROOT / row['verifier_path']).is_file()) self.assertNotIn('answer', row) - self.assertIn('ted osu rotten_tomatoes compass)', (ROOT / 'websyn_start.sh').read_text()) + self.assertIn('ted osu rotten_tomatoes compass walmart_careers)', (ROOT / 'websyn_start.sh').read_text()) self.assertIn("'ted', 'osu', 'rotten_tomatoes'", (ROOT / 'control_server.py').read_text()) - self.assertIn('40000-40022', (ROOT / 'Dockerfile').read_text()) + self.assertIn('40000-40023', (ROOT / 'Dockerfile').read_text()) self.assertTrue((SITE / '.build-generated-seed').is_file()) self.assertTrue((SITE / '.requires-images').is_file()) self.assertTrue((SITE / '.requires-external-cache').is_file()) diff --git a/sites/walmart_careers/.build-generated-seed b/sites/walmart_careers/.build-generated-seed new file mode 100644 index 00000000..5652cb59 --- /dev/null +++ b/sites/walmart_careers/.build-generated-seed @@ -0,0 +1 @@ +The Dockerfile generates instance_seed/walmart_careers.db deterministically from tracked source data and pinned image assets. diff --git a/sites/walmart_careers/.gitignore b/sites/walmart_careers/.gitignore new file mode 100644 index 00000000..a34abb5d --- /dev/null +++ b/sites/walmart_careers/.gitignore @@ -0,0 +1,2 @@ +# Local build/inspection helpers: never shipped with the site. +scripts_dev/ diff --git a/sites/walmart_careers/.requires-images b/sites/walmart_careers/.requires-images new file mode 100644 index 00000000..a598271d --- /dev/null +++ b/sites/walmart_careers/.requires-images @@ -0,0 +1 @@ +Walmart Careers requires its source-backed interface and lifestyle images from the pinned Hugging Face archive. diff --git a/sites/walmart_careers/README.md b/sites/walmart_careers/README.md new file mode 100644 index 00000000..4241bc6a --- /dev/null +++ b/sites/walmart_careers/README.md @@ -0,0 +1,34 @@ +# Walmart Careers mirror + +This directory contains an offline Flask mirror modeled on `https://careers.walmart.com`. In the 24-site registry it runs on container port `40023`. All jobs, stores, requisition identifiers, user accounts, saved roles and applications are deterministic synthetic benchmark data. + +## Runtime + +```bash +uv venv .venv --python 3.12 +uv pip install --python .venv/bin/python -r sites/walmart_careers/requirements.txt +PYTHONHASHSEED=0 .venv/bin/python sites/walmart_careers/seed_data.py +PORT=40023 WALMART_CAREERS_SECRET_KEY="$(python3 -c 'import secrets; print(secrets.token_hex(32))')" \ + .venv/bin/python sites/walmart_careers/app.py +``` + +The Docker build removes any downloaded Walmart seed and regenerates `instance_seed/walmart_careers.db` from `catalog_source.py`. `seed_metadata` version `walmart-careers-v2`, expected row counts, foreign keys and tracked tests reject partial or incompatible state. + +## Assets and provenance + +- `asset_inventory.json` covers the 35 HF-managed runtime images by path, bytes and SHA-256. +- `tracked_asset_inventory.json` covers all tracked runtime icons and the font. +- `provenance.json` classifies catalog and content sources. +- `check_tracked_assets.py` and the root inventory checker run during the Docker build. + +Seventeen image records contain independently recovered direct asset URLs. Eighteen records retain the source page recorded for the contributor's captured derivative because the original PR did not preserve the exact direct asset URL. Those records are marked `source_kind: source_page`; they are not represented as direct-download provenance. + +## Validation + +```bash +python -m pytest sites/walmart_careers/tests sites/walmart_careers/verify/tests -q +python sites/walmart_careers/check_tracked_assets.py +python scripts/check_asset_inventory.py sites/walmart_careers +``` + +The deterministic task verifiers derive target sets from the supplied versioned initial database. See `verify/README.md` for package, workflow, screenshot and state constraints. diff --git a/sites/walmart_careers/_content.py b/sites/walmart_careers/_content.py new file mode 100644 index 00000000..756036be --- /dev/null +++ b/sites/walmart_careers/_content.py @@ -0,0 +1,684 @@ +"""Static CMS-style copy and design constants for the Walmart Careers mirror. + +Nothing in here is runtime data: these are the marketing strings, benefit tiles +and map outlines that the real site serves from AEM. Runtime data (jobs, stores, +users, saved roles, applications) lives in SQLite only. +""" +from __future__ import annotations + +from datetime import date + +# The mirror is frozen against this date. Never call date.today() anywhere in +# the seed or bootstrap path. +MIRROR_REFERENCE_DATE = date(2026, 8, 31) + +SITE_NAME = "Walmart Careers" +COPYRIGHT = "©2026 Walmart Inc." + +HERO_HEADLINE_1 = "Cashiers wanted." +HERO_HEADLINE_2 = "Next move, yours." +SEARCH_PLACEHOLDER = "Search by team, department, keyword" + +VALUES = [ + ("Respect for the individual", "We listen, we support, and we help each other grow."), + ("Service to the customer", "Everything starts with the people who shop with us."), + ("Strive for excellence", "We look for a better way, every single day."), + ("Act with integrity", "We do the right thing, especially when it is hard."), +] + +BENEFIT_ROWS = [ + ("Financial perks", "Enjoy 401(k) matching and stock purchase plans.", "benefit-financial.svg"), + ("Paid time off", "Take a break as needed for vacations, sick leave, holidays, parental leave and more.", "benefit-pto.svg"), + ("Comprehensive health benefits", "Medical, dental, vision and wellness programs for you and your family.", "benefit-health.svg"), + ("Wellbeing programs", "Access mental health resources and assistance programs for life's challenges.", "benefit-wellbeing.svg"), + ("Career growth opportunities", "Training, leadership programs, and clear paths to advance.", "benefit-growth.svg"), +] + +# --------------------------------------------------------------------------- # +# Benefit tiles on the job detail page. Keyed by brand; hourly and salaried +# postings surface a slightly different Live Better U line, exactly as upstream. +# --------------------------------------------------------------------------- # +_WALMART_PLUS_TILE = ( + "Walmart+", + "Free shipping", + "As a Walmart Associate, you're eligible to become a Walmart+ member. Enjoy benefits " + "like free store delivery and shipping, fuel savings, and video streaming. Sam's Club " + "associates are eligible for a Club Membership.", + "tile-walmart-plus.svg", +) +_DISCOUNT_TILE = ( + "Discount Card", + "Get 10% off", + "Walmart associates are eligible for a 10% discount card on all general merchandise " + "items and fresh produce in-store and on select items at Walmart.com. Eligible after " + "90 days of employment.", + "tile-card.svg", +) +_LBU_FIELD_TILE = ( + "Live Better U", + "100% covered", + "Earn a degree or in-demand skills certificates with no debt - Walmart covers 100% of " + "tuition and books. Live Better U offers 60+ programs for Associates to pursue their dreams.", + "tile-graduation.svg", +) +_LBU_CORP_TILE = ( + "Live Better U", + "100% covered", + "Through Live Better U, Walmart and Sam's Club associates can learn critical skills and " + "create pathways for promotion into in-demand jobs within the company. Whether earning a " + "college degree, certificate or high school diploma, Walmart pays for tuition and books.", + "tile-graduation.svg", +) +_ACADEMY_TILE = ( + "Walmart Academy", + "Grow your skills", + "Ready to grow your career? Walmart Academy offers job-specific retail training and " + "leadership courses to help Associates reach their career goals.", + "tile-growth.svg", +) + + +def benefit_tiles_for(brand: str, population: str) -> list[tuple[str, str, str, str]]: + first = _DISCOUNT_TILE if population == "salaried" else _WALMART_PLUS_TILE + second = _LBU_CORP_TILE if population == "salaried" else _LBU_FIELD_TILE + return [first, second, _ACADEMY_TILE] + + +JOB_BENEFIT_ROWS = [ + ("Financial perks", "Enjoy 401(k) matching and stock purchase plans", "benefit-financial.svg"), + ("Wellbeing programs", "Access mental health resources and assistance programs for life's challenges", "benefit-wellbeing.svg"), + ("Paid time off", "Take a break as needed for vacation, sick leave, holidays, parental leave, and more", "benefit-pto.svg"), + ("Career growth opportunities", "Training, leadership programs, and clear paths to advance", "benefit-growth.svg"), + ("Comprehensive health benefits", "Medical, dental, vision, and wellness programs for you and your family", "benefit-health.svg"), +] + +LIFE_AT_WALMART_HEADING = "Life at Walmart" +LIFE_AT_WALMART_QUOTE = ( + "Join us, and help us continue our mission to bring everyday value and support to communities everywhere." +) + +DRUG_FREE_NOTICE = ( + "Walmart is committed to maintaining a drug-free workplace and has a no tolerance policy regarding " + "the use of illegal drugs and alcohol on the job. This policy applies to all employees and aims to " + "create a safe and productive work environment." +) + +HOURLY_PAY_NOTICE = [ + "The actual hourly rate will equal or exceed the required minimum wage applicable to the job location.", + "Additional compensation includes annual or quarterly performance incentives.", + "Additional compensation in the form of premiums may be paid in amounts ranging from $0.35 per hour " + "to $3.00 per hour in specific circumstances. Premiums may be based on schedule, facility, season, " + "or specific work performed. Multiple premiums may apply if applicable criteria are met.", +] + +MIN_QUAL_PREAMBLE = ( + "Outlined below are the required minimum qualifications for this position. If none are listed, " + "there are no minimum qualifications." +) +PREF_QUAL_PREAMBLE = ( + "Outlined below are the optional preferred qualifications for this position. If none are listed, " + "there are no preferred qualifications." +) + +# Salaried detail pages: the boilerplate blocks that follow "What you'll bring" +# on the live corporate postings. Static chrome, keyed by career-area slug for +# the "About ..." paragraph; everything per-posting lives on Job. +SALARIED_ABOUT_AREA = { + "technology": ( + "About Walmart Global Tech", + "Imagine working in an environment where one line of code can make life easier for hundreds of " + "millions of people. That's what we do at Walmart Global Tech. We're a team of software engineers, " + "data scientists, cybersecurity experts and service professionals within the world's leading " + "retailer who make an epic impact and are at the forefront of the next retail disruption. People " + "are why we innovate, and people power our innovations. We are people-led and tech-empowered. We " + "train our team in the skillsets of the future and bring in experts like you to help us grow.", + ), + "corporate": ( + "About Walmart", + "Our home office and corporate teams set the direction for the world's largest retailer: the " + "strategy, the finances, the merchandise, the marketing and the people practices behind more " + "than 10,000 stores and clubs and the associates who run them. The work you do here shows up on " + "shelves and in carts within weeks, not years.", + ), + "students": ( + "About our internships", + "Our internships are paid, project-based and designed to end with a real deliverable. Interns " + "join a team, own a piece of work for the term, present it to leadership and leave with a " + "network across the business. Many of our leaders started as interns.", + ), +} +SALARIED_ABOUT_DEFAULT = ( + "About Walmart", + "Walmart Inc. is the world's largest retailer, serving more than 250 million customers every week " + "through stores, clubs and eCommerce sites in nineteen countries.", +) +SALARIED_HYBRID_NOTE = ( + "We use a hybrid way of working that is primarily in office coupled with virtual when not onsite. " + "Our campuses serve as a hub for collaboration, bring us together for purpose, and deliver on business " + "needs. This approach helps us make quicker decisions, remove location barriers across our global " + "team, and be more flexible in our personal lives." +) +SALARIED_BENEFITS_NOTE = ( + "Beyond our great compensation package, you can receive incentive awards for your performance. Other " + "great perks include 401(k) match, stock purchase plan, paid maternity and parental leave, PTO, " + "multiple health plans, and much more." +) +SALARIED_PAY_NOTE = ( + "At Walmart, we offer competitive pay as well as performance-based bonus awards and other great " + "benefits for a happier mind, body, and wallet. Health benefits include medical, vision and dental " + "coverage. Financial benefits include 401(k), stock purchase and company-paid life insurance. Paid " + "time off benefits include PTO (including sick leave), parental leave, family care leave, " + "bereavement, jury duty, and voting. Other benefits include short-term and long-term disability, " + "company discounts, Military Leave Pay, adoption and surrogacy expense reimbursement, and more." +) +SALARIED_EEO_NOTE = ( + "Walmart, Inc. is an Equal Opportunity Employer - By Choice. We believe we are best equipped to help " + "our associates, customers and the communities we serve live better when we really know them." +) +SALARIED_SCOPE_NOTE = ( + "The above information has been designed to indicate the general nature and level of work performed " + "in the role. It is not designed to contain or be interpreted as a comprehensive inventory of all " + "responsibilities and qualifications required of employees assigned to this job. The full Job " + "Description can be made available as part of the hiring process." +) + +# --------------------------------------------------------------------------- # +# Resources pages +# --------------------------------------------------------------------------- # +LOCATIONS_HEADING = "Our locations" +LOCATIONS_BLURB = ( + "Our hubs spark collaboration and innovation, so you're free to energize and push boundaries " + "from the space that serves you best." +) +LOCATIONS_CLOSING = ( + "Between making an impact at scale and our culture of promoting from within, from coders all the " + "way to cashiers, Walmart is the best place to build a career, period." +) + +HIRING_HEADING = "How we hire" +HIRING_BLURB = ( + "Every career starts with a first step. Whether you're applying for your first job or your next " + "big move, this is the beginning of something new. At Walmart and Sam's Club, the hiring process " + "is about more than landing a role, it's about discovering where you belong, where you can grow, " + "and where your work can make a real difference." +) +HIRING_STEPS = [ + ("1. Find your role", "Search open roles by keyword, career area or location, then save the ones you like."), + ("2. Apply online", "Share your contact details and work history. Most applications take 20-25 minutes."), + ("3. Interview", "A recruiter or hiring manager reaches out, usually within a week of your application."), + ("4. Offer and onboarding", "Accept your offer, complete pre-employment steps and pick your start date."), +] +HIRING_FAQ = [ + ( + "Before you apply", + [ + ( + "Do I need a resume or CV to apply for all Walmart jobs?", + "Not necessarily. A resume or CV is not required to apply, but you will need to provide " + "details about your job history and other information on the application. If you would " + "like to include your resume, LinkedIn profile, portfolio or website, there will be a " + "section where you can add it to your application.", + ), + ( + "How long does it take to fill out an application on average?", + "On average, it takes 20-25 minutes to complete your application for the first time. " + "Subsequent applications will take less time to apply as our system saves your " + "application information.", + ), + ( + "Can I start the application process and finish it later?", + "For hourly roles within the Walmart Online Hiring Center, you have the ability to save " + "your work and log back in at a later time.", + ), + ( + "Can I change my application after submitting?", + "No, you cannot change your application after submitting. Please make sure that " + "everything is finalized before you hit the submit button.", + ), + ], + ), + ( + "After you apply", + [ + ( + "Will I receive confirmation that my application was successfully submitted?", + "Yes. Once you complete your application you will see a confirmation screen with a " + "confirmation number that starts with WMC-.", + ), + ( + "When should I expect to hear back after submitting my application?", + "Timing varies, but we try to respond to applicants within a week of submission.", + ), + ( + "Will I be notified if I am not selected for an interview?", + "Yes, you will be informed if you are not selected for an interview at this time.", + ), + ( + "Do you provide reasonable accommodations during the application process?", + "Yes, reach out to your manager, recruiter or recruiting coordinator about any needs " + "you have. We are happy to do what we can to support you.", + ), + ], + ), +] + +TERMS_HEADING = "Terms & Conditions" +TERMS_SECTIONS = [ + ( + "About this mirror", + "This is an offline WebHarbor mirror of careers.walmart.com built for agent benchmarking. " + "No application submitted here reaches Walmart Inc., and no data leaves the container.", + ), + ( + "Candidate accounts", + "Accounts created on this mirror exist only inside the local database and are removed whenever " + "the environment is reset to its seed state.", + ), + ( + "Applications", + "Submitting an application records a row in the local database and returns a confirmation number " + "in the form WMC-000000. It creates no relationship, express or implied, with Walmart Inc.", + ), + ( + "Accuracy of postings", + "Job postings, pay ranges, store addresses and requisition IDs shown here are synthetic mirror " + "data modelled on the structure of the real site.", + ), +] + +# --------------------------------------------------------------------------- # +# Footer +# --------------------------------------------------------------------------- # +FOOTER_CAREER_LINKS = [ + ("Stores and Clubs", "stores-and-clubs"), + ("Supply Chain and Transportation", "supply-chain-and-transportation"), + ("Healthcare", "healthcare"), + ("Technology", "technology"), + ("Corporate", "corporate"), +] +FOOTER_BRANDS = ["Walmart", "Sam's Club", "VIZIO"] +FOOTER_SOCIAL = [ + ("Facebook", "social-facebook.svg"), + ("Instagram", "social-instagram.svg"), + ("LinkedIn", "social-linkedin.svg"), + ("X", "social-x.svg"), + ("YouTube", "social-youtube.svg"), + ("Glassdoor", "social-glassdoor.svg"), +] +FOOTER_EEO = ( + "Walmart, Inc. is an Equal Opportunity Employer. We believe we are best equipped to help our " + "associates, customers, and the communities we serve live better when we really know them. That " + "means understanding, respecting, and valuing unique styles, experiences, identities, abilities, " + "ideas and opinions- while welcoming all people. Walmart Inc. participates in E-verify. Learn more " + "about applicant rights under Federal Employment Laws." +) +FOOTER_BENEFITS_NOTE = ( + "Eligibility for benefits depends on your job classification, and benefits are subject to specific " + "plan or program terms. For more information about your benefits options, please see the Associate " + "Benefits Book at One.Walmart.com/BenefitsBook." +) + +EMPTY_FUTURE_ROLES = ( + "Future roles are not part of this mirror. Every posting in this environment is an open role you " + "can browse, save and apply to from the Open roles tab." +) +EMPTY_CONTENT_TAB = ( + "Content search is not part of this mirror. Use the Open roles tab, the career area pages or the " + "Resources pages to explore this site." +) + +# --------------------------------------------------------------------------- # +# Coarse lat/lng outlines used by the deterministic server-rendered cluster map. +# Points are (lng, lat). +# --------------------------------------------------------------------------- # +US_OUTLINE = [ + (-124.7, 48.4), (-123.0, 48.2), (-122.6, 47.0), (-124.0, 46.3), (-124.1, 43.7), + (-124.4, 42.0), (-124.2, 40.4), (-122.4, 37.8), (-121.9, 36.6), (-120.6, 34.6), + (-118.4, 33.7), (-117.1, 32.5), (-114.7, 32.7), (-111.1, 31.3), (-108.2, 31.3), + (-106.5, 31.8), (-104.9, 30.6), (-103.1, 29.0), (-101.4, 29.8), (-99.1, 26.4), + (-97.1, 25.9), (-97.4, 28.0), (-95.0, 29.1), (-93.8, 29.7), (-91.0, 29.2), + (-89.4, 29.0), (-89.0, 30.2), (-87.5, 30.3), (-85.0, 29.7), (-84.0, 30.1), + (-82.8, 27.9), (-81.8, 25.9), (-80.1, 25.2), (-80.1, 27.0), (-81.4, 30.7), + (-80.8, 32.0), (-78.9, 33.7), (-75.7, 35.2), (-76.0, 36.9), (-75.1, 38.3), + (-74.0, 39.7), (-73.9, 40.6), (-71.9, 41.3), (-70.0, 41.7), (-70.2, 42.6), + (-70.8, 43.2), (-69.0, 43.9), (-67.0, 44.8), (-67.8, 45.7), (-69.2, 47.5), + (-71.5, 45.0), (-74.7, 45.0), (-76.9, 43.3), (-79.2, 43.4), (-82.4, 41.7), + (-83.1, 42.2), (-82.5, 45.3), (-84.4, 46.5), (-87.6, 46.0), (-88.0, 48.2), + (-89.5, 48.0), (-95.2, 49.0), (-104.0, 49.0), (-116.0, 49.0), (-123.0, 49.0), +] +PR_OUTLINE = [ + (-67.3, 18.5), (-66.4, 18.5), (-65.6, 18.4), (-65.6, 17.9), (-66.6, 17.9), (-67.3, 18.1), +] + + +# --------------------------------------------------------------------------- # +# Header navigation. The dropdown groups mirror the live top bar: +# Career areas | Brands | Resources | About Us | Military. +# --------------------------------------------------------------------------- # +# (label, brand filter value) for the Brands dropdown. +NAV_BRANDS = [ + ("Walmart", "Walmart"), + ("Sam's Club", "Sam's Club"), + ("VIZIO", "Vizio"), +] +# (label, endpoint) for the Resources dropdown. +NAV_RESOURCES = [ + ("How we hire", "resources_hiring"), + ("Office Locations", "resources_location"), + ("Terms & Conditions", "resources_terms"), +] + +ABOUT_HEADING = "About Us" +ABOUT_BLURB = ( + "Walmart is a people-led, tech-powered omnichannel retailer. Around the world our associates " + "serve customers in stores, clubs, distribution centers and online, and every one of those jobs " + "is a step toward something greater." +) +ABOUT_SECTIONS = [ + ( + "Our purpose", + "We save people money so they can live better. That purpose has guided every decision since " + "Sam Walton opened the first store in Rogers, Arkansas, and it still shapes how we hire, how " + "we promote, and how we invest in the communities we serve.", + ), + ( + "How we work", + "We are people-led and tech-powered. Associates in stores, clubs, supply chain and the home " + "office work with the same tools and the same data, so a good idea can start anywhere and " + "reach millions of customers quickly.", + ), + ( + "Where you can grow", + "About three quarters of our salaried store managers began as hourly associates. Live Better U " + "pays for tuition, books and fees, and Walmart Academy runs skills training in every market we " + "operate in.", + ), +] + +# --------------------------------------------------------------------------- # +# US state / territory names, used to resolve a plain state in the location box +# ("PR", "Puerto Rico", "Ohio") into a state-wide result set. +# --------------------------------------------------------------------------- # +STATE_NAMES = { + "AL": "Alabama", "AK": "Alaska", "AZ": "Arizona", "AR": "Arkansas", + "CA": "California", "CO": "Colorado", "CT": "Connecticut", "DC": "District of Columbia", + "DE": "Delaware", "FL": "Florida", "GA": "Georgia", "HI": "Hawaii", + "IA": "Iowa", "ID": "Idaho", "IL": "Illinois", "IN": "Indiana", + "KS": "Kansas", "KY": "Kentucky", "LA": "Louisiana", "MA": "Massachusetts", + "MD": "Maryland", "ME": "Maine", "MI": "Michigan", "MN": "Minnesota", + "MO": "Missouri", "MS": "Mississippi", "MT": "Montana", "NC": "North Carolina", + "ND": "North Dakota", "NE": "Nebraska", "NH": "New Hampshire", "NJ": "New Jersey", + "NM": "New Mexico", "NV": "Nevada", "NY": "New York", "OH": "Ohio", + "OK": "Oklahoma", "OR": "Oregon", "PA": "Pennsylvania", "PR": "Puerto Rico", + "RI": "Rhode Island", "SC": "South Carolina", "SD": "South Dakota", "TN": "Tennessee", + "TX": "Texas", "UT": "Utah", "VA": "Virginia", "VI": "U.S. Virgin Islands", + "VT": "Vermont", "WA": "Washington", "WI": "Wisconsin", "WV": "West Virginia", + "WY": "Wyoming", +} +STATE_CODES_BY_NAME = {name.lower(): code for code, name in sorted(STATE_NAMES.items())} + + +# --------------------------------------------------------------------------- # +# Home page chrome below the fold: the values bento, the milestone badges and +# the "See our associates in action" strip. Static marketing copy only. +# --------------------------------------------------------------------------- # +HOME_INTRO_HEADLINE = ("Grow your future.", "Make an impact.") +HOME_INTRO_CTA = "See our values in action" +BENEFITS_ASIDE = "That's just the beginning. We offer more perks specific to your work location and role." +BENEFITS_CTA = "Learn more about benefits" +MILESTONE_HEADING = "Here, every job is a step toward something greater" +# (figure sentence, badge label, style) — style picks the badge colour scheme. +MILESTONE_BADGES = [ + ("$1 billion invested in associate career training and development", "", "sky"), + ("75% of salaried managers began as hourly associates", "5 YEARS", "spark"), + ("300,000 associates have earned a 10+ year badge", "10 YEARS", "navy"), + ("120,000 U.S. associates have participated in Live Better U", "20 YEARS", "blue"), +] +ASSOCIATES_HEADING = "See our associates in action" +ASSOCIATES_BLURB = ( + "Every day, Walmart associates step up - solving problems, serving communities, and making a " + "difference. They don't just do the job; they bring it to life." +) +FIND_ROLE_HEADING = "Find the role that's a perfect fit" +FIND_ROLE_PLACEHOLDER = "Search by team, department, or keyword" + +# --------------------------------------------------------------------------- # +# Career-area page chrome (per area slug): the lower sections of the L1 pages. +# --------------------------------------------------------------------------- # +AREA_PAGE = { + "stores-and-clubs": { + "tiles": ("Purpose", "Growth", "Pride"), + "headline": "You power the experience for millions", + "cta": "See all stores and clubs roles", + "photos": ("area-stores-3.jpg", "area-stores-2.jpg"), + "quote": "At Walmart and Sam's Club, our stores and clubs are powered by people, dedicated " + "associates working together to create exceptional experiences for the communities " + "we serve.", + "testimonials": [ + ("Curtis", "Store Manager", "Every shift is a chance to make someone's day a little easier."), + ("D'Rogelio", "Store Manager", "You can be you in this environment and still succeed."), + ("Jamaily", "Club Manager", "I started on the floor. Now I run the building."), + ], + }, + "supply-chain-and-transportation": { + "tiles": ("Safety", "Scale", "Momentum"), + "headline": "Move what matters, at scale", + "cta": "See all supply chain roles", + "photos": ("area-supply-chain-2.jpg", "supply-drone.jpg"), + "quote": "Our supply chain associates move millions of items a day through a network that " + "reaches nearly every community in the country - and they do it safely.", + "testimonials": [ + ("Caleb", "Maintenance Tech", "The equipment is the most advanced I've worked on anywhere."), + ("Renee", "Yard Driver", "I know exactly how my work gets product to a shelf."), + ("Marcus", "Area Manager", "We promote from the floor. That's not a slogan here."), + ], + }, + "healthcare": { + "tiles": ("Care", "Community", "Growth"), + "headline": "Care for the communities you call home", + "cta": "See all healthcare roles", + "photos": ("area-healthcare.jpg", "jobhero-wm-2.jpg"), + "quote": "Our pharmacies, vision centers and clinics put affordable care within a short drive " + "of most of the country - and our associates make it personal.", + "testimonials": [ + ("Yasinya", "Pharmacy Tech", "Patients know my name. That's the part I love."), + ("Andre", "Optician", "Every fitting is a small problem to solve well."), + ("Priya", "Pharmacy Manager", "Walmart paid for my certification through Live Better U."), + ], + }, + "technology": { + "tiles": ("Belonging", "Impact"), + "headline": "Tech with real-world impact", + "cta": "See all technology roles", + "photos": ("area-technology-2.jpg", "supply-drone.jpg"), + "quote": "Our vision is strong here. Walmart Global Tech works at the forefront of " + "cutting-edge technologies inspired by the vision of transforming retail tech.", + "hubs_heading": "Four hubs. One mission. Endless possibilities", + "hubs_blurb": "Our hubs spark collaboration and innovation, so you're free to energize and push " + "boundaries from the space that serves you best.", + "testimonials": [ + ("Tatiana", "Software Engineer (iOS)", "The scale of what ships every week still amazes me."), + ("Christopher", "Senior Manager, Food Media Insights", + "We're data geeks, and the depth we get to explore here keeps us excited every single day."), + ("Antony", "Yield Manager", "I get to work on problems no other retailer has."), + ], + }, + "corporate": { + "tiles": ("Curiosity", "Ownership", "Impact"), + "headline": "Shape how the world shops", + "cta": "See all corporate roles", + "photos": ("area-corporate-2.jpg", "jobhero-corp-3.jpg"), + "quote": "From merchandising to finance to people, our home office teams make decisions that " + "reach 240 million customers a week.", + "hubs_heading": "Hubs built for the way you work", + "hubs_blurb": "Bentonville, Sunnyvale, Hoboken and Dallas: pick the space that serves you best.", + "testimonials": [ + ("Jorden", "Associate Merchant", "I own a category. At 26. That doesn't happen elsewhere."), + ("Nina", "Finance Manager", "The numbers are big, but the teams are small and close."), + ("Sam", "People Partner", "We hire for potential and then we invest in it."), + ], + }, + "Military": { + "tiles": ("Transition", "Translate", "Thrive"), + "headline": "Walmart supports Veterans", + "cta": "See all opportunities", + "photos": ("area-military.jpg", "military-banner.png"), + "quote": "Every day, thousands of veterans build careers at Walmart. Learn more about our " + "commitment to veterans and military families.", + "testimonials": [ + ("Mark", "Veteran, Store Coach", "My leadership experience translated on day one."), + ("Kim", "Store Manager", "Walmart's not only committed to the veteran - veteran spouses have just the same opportunity."), + ("Jeremy", "Club Manager", "SkillBridge got me in the door. The team kept me here."), + ], + }, +} +AREA_PAGE_DEFAULT = { + "tiles": ("Purpose", "Growth", "Pride"), + "headline": "Grow your future. Make an impact.", + "cta": "See all open roles", + "photos": ("area-stores-3.jpg", "area-stores-2.jpg"), + "quote": LIFE_AT_WALMART_QUOTE, + "testimonials": [], +} +INSPIRATION_HEADING = "Inspiration in every role" + +# Locations page hero and the promo block on the saved-roles page. +LOCATIONS_HERO_IMAGE = "loc-silicon-valley.jpg" +SAVED_PROMO_HEADLINE = ("Get more out of", "Walmart Careers") +SAVED_PROMO_BLURB = ( + "With an account you get role recommendations, create job alerts, and view your application " + "status from a personalized dashboard." +) +SAVED_PROMO_IMAGE = "area-healthcare.jpg" +SAVED_EMPTY_NOTE = "You have no saved roles." + +# Footer links on the stripped sign-in / register layout. +AUTH_FOOTER_LINKS = [ + "Give feedback", "Terms of Use", "Privacy Notice", "California Supply Chain Act", + "Your Privacy Choices", "Customer Privacy Center", "Notice at Collection", +] +AUTH_COPYRIGHT = "© 2026 Walmart. All Rights Reserved." + + +# --------------------------------------------------------------------------- # +# "Life at Walmart" on the detail pages: a lead sentence, the paragraphs beside +# the photo, the paragraphs under it, the blue quote band and the closing lines. +# Hourly postings use the field copy, salaried postings the home-office copy. +# --------------------------------------------------------------------------- # +LIFE_AT_WALMART_FIELD = { + "lead": "At Walmart, you're welcome for who you are, no matter your background, experiences, " + "or perspectives.", + "left": [ + "Our stores and services are for everyone, and so is our workplace. We believe different " + "experiences drive our ability to better serve our communities and deliver affordable " + "products across the nation.", + "Here, your unique insights and ideas are encouraged, valued, and essential to creating a " + "forward-thinking company that thrives on fresh ideas and dedicated teamwork.", + ], + "right": [], + "band": "Since our founding, we've focused on bringing affordable essentials to families " + "everywhere, and today, Walmart is one of the most recognizable names in retail worldwide.", + "closing": LIFE_AT_WALMART_QUOTE, + "note": "We're driven by a commitment to make life better for millions of customers and support " + "our associates with opportunities to grow, learn, and advance.", + "photo": "jobhero-wm-4.jpg", +} +LIFE_AT_WALMART_CORP = { + "lead": "Imagine a workplace surrounded by innovation. At Walmart's new Home Office in " + "Bentonville, Arkansas, we're redefining what it means to work at a global leader.", + "left": [ + "Set on 350 acres of thoughtfully revitalized land, our new campus seamlessly integrates " + "the charm of Northwest Arkansas with cutting edge design and technology. From biking " + "trails and outdoor courtyards to flexible, tech-enabled workspaces, every detail reflects " + "our commitment to sustainability, connection, and culture.", + ], + "right": [ + "With amenities like on-site childcare at our Little Squiggles Children's Enrichment " + "Center, the Walton Family Whole Health & Fitness Center, and a vibrant food hall " + "featuring local and international favorites, we're creating a space where work-life " + "balance isn't just a goal - it's a reality.", + "Beyond the campus, Bentonville offers a dynamic lifestyle with world-class dining, art at " + "the Crystal Bridges Museum, and countless outdoor activities.", + ], + "band": "Whether you're exploring High South cuisine, enjoying live performances at our new " + "amphitheater, or cycling the Razorback Greenway, you'll experience the perfect blend " + "of small-town charm and big-city amenities.", + "closing": "Join us at Walmart and grow your career alongside a community that feels like home.", + "note": "This isn't just a workplace - it's a destination for leaders eager to make a difference.", + "photo": "life-home-office.jpg", +} + +# Stores & Clubs L1: the "Meet Brandon" day-in-the-life block under the bento. +MEET_STORE_COACH = { + "name": "Brandon", + "kicker": "Day in the life", + "role": "Walmart store coach", + "blurb": "As a store coach, Brandon leads with energy, empathy, and focus. In this video, he " + "shares what it takes to guide a team in one of Walmart's busiest stores - balancing " + "daily priorities, supporting associates, and helping people grow.", +} + +# Military L1: the two feature rows and the three program tiles under the hero. +MILITARY_FEATURES = [ + ( + "SkillBridge: Your Transition, Supported", + "Preparing to separate from active duty? Through the DoD SkillBridge program, you can build " + "career-ready skills with structured training and real-world experience while you're still " + "serving. Explore opportunities designed to help you translate your military strengths into " + "a long-term career at Walmart.", + "Explore opportunities now", + "area-military.jpg", + ), + ( + "Military skills translator", + "Translate your military experience into civilian job skills. Use our tool to discover the " + "best career opportunities that align with your unique qualifications.", + "Explore now", + "jobhero-corp-3.jpg", + ), +] +MILITARY_PROGRAMS = [ + ( + "Discover Walmart: job simulations", + "Experience various roles at Walmart through our flexible job simulations. Choose modules, " + "upskill at your pace, and gain insights to succeed in the application process. Explore " + "multiple career paths at Walmart.", + "jobhero-corp-2.jpg", + ), + ( + "Internships: kickstart your career", + "Our internship programs offer valuable opportunities for individuals at any stage of their " + "education or career. Gain experience in various fields, apply your unique skills, receive " + "mentorship, and get hands-on training that sets you apart in your chosen career path.", + "area-students.png", + ), + ( + "Join our talent network", + "Sign up for our talent network, participate in one of our engaging hiring events or " + "military connected workshops designed to showcase diverse career paths and provide " + "opportunities to network with Walmart professionals.", + "area-corporate.jpg", + ), +] + +# How-we-hire page: hero photo, the intro beside each FAQ group, the job simulator block. +HIRING_HERO_IMAGE = "area-corporate-2.jpg" +HIRING_HERO_CTA = "Explore something new" +HIRING_FAQ_INTROS = [ + ("We're here to help you put your best foot forward. Get tips and guidance to feel confident " + "as you take the first step toward a role that's right for you.", "benefit-wellbeing.svg"), + ("You've taken a big step and we're glad you did! Here's what to expect next, plus answers to " + "common questions to help you stay informed and encouraged along the way.", "benefit-pto.svg"), +] +HIRING_SIMULATOR = { + "heading": "Experience a day in the role", + "blurb": "Want to know what a role at Walmart and Sam's Club is really like? Our interactive job " + "simulations give you a chance to preview the role, showcase your skills, and see if " + "it's a good fit for you.", + "cta": "Job simulator", + "photo": "jobhero-corp-2.jpg", +} + +# Apply flow: the first-party contact step's heading and hint. +APPLY_HEADING = "Let us know how to contact you" +APPLY_EMAIL_HINT = "Avoid using an email address you share with others" diff --git a/sites/walmart_careers/_health.py b/sites/walmart_careers/_health.py new file mode 100644 index 00000000..cd932bcc --- /dev/null +++ b/sites/walmart_careers/_health.py @@ -0,0 +1,5 @@ +"""Per-site health probe (optional, called by control_server).""" + + +def health(): + return {"ok": True, "site": "walmart_careers"} diff --git a/sites/walmart_careers/app.py b/sites/walmart_careers/app.py new file mode 100644 index 00000000..05c51db7 --- /dev/null +++ b/sites/walmart_careers/app.py @@ -0,0 +1,1354 @@ +"""Walmart Careers local mirror for WebHarbor.""" +from __future__ import annotations + +import json +import math +import os +import re +import secrets +import sys +from datetime import datetime, timedelta +from pathlib import Path +from urllib.parse import unquote, urlencode, urlsplit + +from flask import ( + Flask, + abort, + flash, + jsonify, + redirect, + render_template, + request, + session, + url_for, +) +from flask_login import ( + LoginManager, + UserMixin, + current_user, + login_required, + login_user, + logout_user, +) +from flask_sqlalchemy import SQLAlchemy +from flask_wtf.csrf import CSRFProtect +from sqlalchemy import event +from sqlalchemy.engine import Engine +from sqlalchemy.exc import IntegrityError +from werkzeug.security import check_password_hash, generate_password_hash + +import _content as content + +BASE_DIR = Path(__file__).resolve().parent +INSTANCE_DIR = BASE_DIR / "instance" +DB_PATH = INSTANCE_DIR / "walmart_careers.db" + +INSTANCE_DIR.mkdir(parents=True, exist_ok=True) + +app = Flask(__name__, instance_path=str(INSTANCE_DIR)) +app.config.update( + SECRET_KEY=os.environ.get("WALMART_CAREERS_SECRET_KEY") or os.urandom(32), + SQLALCHEMY_DATABASE_URI=f"sqlite:///{DB_PATH}", + SQLALCHEMY_TRACK_MODIFICATIONS=False, + WTF_CSRF_TIME_LIMIT=7200, + MAX_CONTENT_LENGTH=256 * 1024, + SESSION_COOKIE_HTTPONLY=True, + SESSION_COOKIE_SAMESITE="Lax", + SESSION_COOKIE_SECURE=os.environ.get("WALMART_CAREERS_SECURE_COOKIE") == "1", + PERMANENT_SESSION_LIFETIME=timedelta(hours=12), +) + + +@event.listens_for(Engine, "connect") +def enable_sqlite_foreign_keys(connection, _record) -> None: + cursor = connection.cursor() + cursor.execute("PRAGMA foreign_keys=ON") + cursor.close() + +db = SQLAlchemy(app) +csrf = CSRFProtect(app) +login_manager = LoginManager(app) +login_manager.login_view = "login" +login_manager.login_message = "Sign in to continue." +login_manager.login_message_category = "info" + +DEMO_PASSWORD = "TestPass123!" +EMAIL_PATTERN = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$") +PAGE_SIZE = 10 +MAX_QUERY_LENGTH = 160 +MAX_LOCATION_LENGTH = 80 +MAX_PASSWORD_LENGTH = 256 +SEED_VERSION = "walmart-careers-v2" + +SHIFT_VALUES = [ + "Weekday Day", + "Weekday Evening", + "Weekday Overnight", + "Weekend Day", + "Weekend Evening", + "Weekend Overnight", + "Flex", +] +BRAND_VALUES = ["Vizio", "Walmart", "Sam's Club"] +EMPLOYMENT_TYPE_VALUES = ["Full time", "Part time", "Intern"] +RATE_VALUES = ["Salaried", "Hourly"] +RADIUS_VALUES = [5, 15, 25, 60] +SORT_VALUES = ["relevance", "most_recent"] + +STOPWORDS = { + "a", "an", "and", "at", "for", "in", "of", "on", "or", "the", "to", "with", + "jobs", "job", "roles", "role", "near", "me", "all", +} + + +# --------------------------------------------------------------------------- # +# Models +# --------------------------------------------------------------------------- # +class SeedMetadata(db.Model): + __tablename__ = "seed_metadata" + key = db.Column(db.String(64), primary_key=True) + value = db.Column(db.String(160), nullable=False) + + +class Area(db.Model): + __tablename__ = "areas" + id = db.Column(db.Integer, primary_key=True) + slug = db.Column(db.String(64), unique=True, nullable=False) + name = db.Column(db.String(80), nullable=False) + display_order = db.Column(db.Integer, nullable=False, default=0) + blurb = db.Column(db.Text, nullable=False, default="") + hero_image = db.Column(db.String(120), nullable=False, default="") + has_index_page = db.Column(db.Boolean, nullable=False, default=True) + is_filterable = db.Column(db.Boolean, nullable=False, default=True) + + categories = db.relationship( + "Category", backref="area", lazy="select", + order_by="Category.display_order", + ) + + @property + def url(self) -> str: + return url_for("career_area", slug=self.slug) + + @property + def nav_url(self) -> str: + """Where the "Career areas" menu points: the area page when there is one, + otherwise straight to that area's open roles.""" + if self.has_index_page: + return url_for("career_area", slug=self.slug) + return url_for("results", area=self.slug) + + +class Category(db.Model): + __tablename__ = "categories" + id = db.Column(db.Integer, primary_key=True) + area_id = db.Column(db.Integer, db.ForeignKey("areas.id"), nullable=False) + name = db.Column(db.String(120), nullable=False) + slug = db.Column(db.String(120), nullable=False) + display_order = db.Column(db.Integer, nullable=False, default=0) + + __table_args__ = ( + db.UniqueConstraint("area_id", "slug", name="uq_category_area_slug"), + ) + + +class Store(db.Model): + __tablename__ = "stores" + id = db.Column(db.Integer, primary_key=True) + store_number = db.Column(db.String(16), unique=True, nullable=False) + banner = db.Column(db.String(64), nullable=False) + location_name = db.Column(db.String(120), nullable=False) + street = db.Column(db.String(160), nullable=False) + city = db.Column(db.String(80), nullable=False) + state = db.Column(db.String(2), nullable=False) + zip = db.Column(db.String(12), nullable=False) + lat = db.Column(db.Float, nullable=False) + lng = db.Column(db.Float, nullable=False) + is_hub = db.Column(db.Boolean, nullable=False, default=False) + is_office = db.Column(db.Boolean, nullable=False, default=False) + hub_name = db.Column(db.String(80), nullable=True) + hub_blurb = db.Column(db.Text, nullable=True) + hub_image = db.Column(db.String(80), nullable=True) + + @property + def banner_line(self) -> str: + return f"{self.banner} #{self.store_number}" + + @property + def city_state(self) -> str: + return f"{self.city}, {self.state}" + + +class Job(db.Model): + __tablename__ = "jobs" + job_id = db.Column(db.String(32), primary_key=True) + population = db.Column(db.String(16), nullable=False) # salaried | hourly + title = db.Column(db.String(160), nullable=False) + brand = db.Column(db.String(24), nullable=False) + store_id = db.Column(db.Integer, db.ForeignKey("stores.id"), nullable=False) + area_id = db.Column(db.Integer, db.ForeignKey("areas.id"), nullable=False) + category_id = db.Column(db.Integer, db.ForeignKey("categories.id"), nullable=False) + shifts_json = db.Column(db.Text, nullable=False, default="[]") + employment_type = db.Column(db.String(16), nullable=False) + pay_frequency = db.Column(db.String(8), nullable=False) # Hourly | Annual + min_pay = db.Column(db.Numeric(10, 2), nullable=False) + max_pay = db.Column(db.Numeric(10, 2), nullable=False) + posted_date = db.Column(db.Date, nullable=False) + sort_rank = db.Column(db.Integer, nullable=False, default=0) + is_trending = db.Column(db.Boolean, nullable=False, default=False) + summary = db.Column(db.Text, nullable=False, default="") + description = db.Column(db.Text, nullable=False, default="") + about_team = db.Column(db.Text, nullable=True) # salaried only + additional_description_json = db.Column(db.Text, nullable=True) + hashtag = db.Column(db.String(48), nullable=True) + shift_time = db.Column(db.String(120), nullable=True) + positions_available = db.Column(db.Integer, nullable=True) + min_age_note = db.Column(db.Boolean, nullable=False, default=False) + worker_type = db.Column(db.String(48), nullable=True) + job_posting_id = db.Column(db.String(48), nullable=True) + min_qualifications_json = db.Column(db.Text, nullable=True) + preferred_qualifications = db.Column(db.Text, nullable=True) + hero_images_json = db.Column(db.Text, nullable=False, default="[]") + + __table_args__ = ( + db.CheckConstraint("population IN ('salaried', 'hourly')", name="ck_jobs_population"), + db.CheckConstraint("brand IN ('Vizio', 'Walmart', 'Sam''s Club')", name="ck_jobs_brand"), + db.CheckConstraint("employment_type IN ('Full time', 'Part time', 'Intern')", name="ck_jobs_employment_type"), + db.CheckConstraint("pay_frequency IN ('Hourly', 'Annual')", name="ck_jobs_pay_frequency"), + db.CheckConstraint("min_pay >= 0 AND max_pay >= min_pay", name="ck_jobs_pay_range"), + db.CheckConstraint("positions_available IS NULL OR positions_available > 0", name="ck_jobs_positions"), + ) + + store = db.relationship("Store", lazy="joined") + area = db.relationship("Area", lazy="joined") + category = db.relationship("Category", lazy="joined") + + # -- derived display helpers ------------------------------------------- # + @property + def shifts(self) -> list[str]: + return json.loads(self.shifts_json or "[]") + + @property + def shift_label(self) -> str: + values = self.shifts + if len(values) == 1: + return values[0] + return "Multiple shifts" + + @property + def is_salaried(self) -> bool: + return self.population == "salaried" + + @property + def rate_label(self) -> str: + return "Salaried" if self.is_salaried else "Hourly" + + @property + def pay_suffix(self) -> str: + return "/yr" if self.pay_frequency == "Annual" else "/hr" + + @property + def pay_range(self) -> str: + if self.pay_frequency == "Annual": + return f"${int(self.min_pay):,} - ${int(self.max_pay):,}/yr" + return f"${float(self.min_pay):,.2f} - ${float(self.max_pay):,.2f}/hr" + + @property + def additional_description(self) -> list[str]: + return json.loads(self.additional_description_json or "[]") + + @property + def min_qualifications(self) -> list[str]: + return json.loads(self.min_qualifications_json or "[]") + + @property + def hero_images(self) -> list[str]: + return json.loads(self.hero_images_json or "[]") + + @property + def url(self) -> str: + return url_for("job_detail", job_id=self.job_id) + + @property + def search_blob(self) -> str: + """Fields the scored search reads. Description is deliberately excluded.""" + return " ".join( + [ + self.title, + self.category.name if self.category else "", + self.area.name if self.area else "", + self.store.banner if self.store else "", + self.store.city if self.store else "", + self.store.state if self.store else "", + self.brand, + self.hashtag or "", + ] + ) + + +class User(UserMixin, db.Model): + __tablename__ = "users" + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(160, collation="NOCASE"), unique=True, nullable=False) + username = db.Column(db.String(80, collation="NOCASE"), unique=True, nullable=False) + display_name = db.Column(db.String(120), nullable=False, default="") + first_name = db.Column(db.String(80), nullable=False, default="") + last_name = db.Column(db.String(80), nullable=False, default="") + phone = db.Column(db.String(32), nullable=False, default="") + city = db.Column(db.String(80), nullable=False, default="") + state = db.Column(db.String(2), nullable=False, default="") + password_hash = db.Column(db.String(256), nullable=False) + created_at = db.Column(db.DateTime, nullable=False) + + def check_password(self, raw: str) -> bool: + return check_password_hash(self.password_hash, raw) + + +class SavedJob(db.Model): + __tablename__ = "saved_jobs" + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) + job_id = db.Column(db.String(32), db.ForeignKey("jobs.job_id"), nullable=False) + saved_at = db.Column(db.DateTime, nullable=False) + + __table_args__ = ( + db.UniqueConstraint("user_id", "job_id", name="uq_saved_user_job"), + ) + + job = db.relationship("Job", lazy="joined") + + +class ApplicationDraft(db.Model): + __tablename__ = "application_drafts" + id = db.Column(db.Integer, primary_key=True) + token = db.Column(db.String(64), unique=True, nullable=False) + job_id = db.Column(db.String(32), db.ForeignKey("jobs.job_id"), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True) + email = db.Column(db.String(160), nullable=False) + first_name = db.Column(db.String(80), nullable=False) + last_name = db.Column(db.String(80), nullable=False) + phone = db.Column(db.String(32), nullable=False) + created_at = db.Column(db.DateTime, nullable=False) + + +class Application(db.Model): + __tablename__ = "applications" + id = db.Column(db.Integer, primary_key=True) + job_id = db.Column(db.String(32), db.ForeignKey("jobs.job_id"), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True) + email = db.Column(db.String(160), nullable=False) + first_name = db.Column(db.String(80), nullable=False) + last_name = db.Column(db.String(80), nullable=False) + phone = db.Column(db.String(32), nullable=False) + status = db.Column(db.String(32), nullable=False, default="Submitted") + confirmation_no = db.Column(db.String(32), unique=True, nullable=False) + submitted_at = db.Column(db.DateTime, nullable=False) + + __table_args__ = ( + db.CheckConstraint("status IN ('Submitted')", name="ck_applications_status"), + ) + + job = db.relationship("Job", lazy="joined") + + +@login_manager.user_loader +def load_user(user_id: str): + if not str(user_id).isdigit(): + return None + return db.session.get(User, int(user_id)) + + +# --------------------------------------------------------------------------- # +# Helpers +# --------------------------------------------------------------------------- # +def dumps_json(value) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=False, separators=(",", ":")) + + +def confirmation_for(application_id: int) -> str: + return f"WMC-{application_id:06d}" + + +def tokenize(text: str) -> list[str]: + return [t for t in re.split(r"[^a-z0-9']+", (text or "").lower()) if t and t not in STOPWORDS] + + +def haversine_miles(lat1: float, lng1: float, lat2: float, lng2: float) -> float: + radius = 3958.7613 + p1, p2 = math.radians(lat1), math.radians(lat2) + dp = p2 - p1 + dl = math.radians(lng2 - lng1) + a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2 + return 2 * radius * math.asin(math.sqrt(a)) + + +def safe_next(raw: str | None) -> str | None: + """Return a canonical same-origin path/query target or ``None``.""" + if not raw or len(raw) > 2048 or any(ord(char) < 32 for char in raw): + return None + decoded = raw + for _ in range(3): + expanded = unquote(decoded) + if expanded == decoded: + break + decoded = expanded + if decoded.startswith("//"): + return None + parsed = urlsplit(decoded) + if parsed.scheme or parsed.netloc or not parsed.path.startswith("/"): + return None + if parsed.path.startswith("//") or "\\" in decoded: + return None + return parsed.path + (("?" + parsed.query) if parsed.query else "") + + +def bounded_text(value: str | None, field: str, maximum: int, *, required: bool = False) -> tuple[str, str | None]: + text = (value or "").strip() + if required and not text: + return text, f"Enter your {field}." + if len(text) > maximum: + return text, f"{field.capitalize()} must be {maximum} characters or fewer." + if any(ord(char) < 32 for char in text): + return text, f"{field.capitalize()} contains unsupported control characters." + return text, None + + +def single_query_arg(name: str, default: str = "") -> str: + values = request.args.getlist(name) + if len(values) > 1: + abort(400, description=f"duplicate query parameter: {name}") + return values[0] if values else default + + +def resolve_location(raw: str) -> dict | None: + """Resolve free text typed into the location box. + + Returns ``None`` when nothing matches, otherwise a scope dict: + + * ``{"kind": "state", "state": "PR", "label": "Puerto Rico"}`` + when the text names a whole state or territory — the result set is then + every role in that state, with no radius applied. + * ``{"kind": "store", "store": , "label": "Rochester, NY"}`` when the + text names a city, a "City, ST" pair or a ZIP — the radius then applies. + """ + text = (raw or "").strip() + if not text: + return None + stores = Store.query.order_by(Store.store_number).all() + + def state_scope(code: str) -> dict | None: + if code not in content.STATE_NAMES: + return None + return { + "kind": "state", + "state": code, + "label": content.STATE_NAMES[code], + } + + # Whole-state searches: "PR", "Puerto Rico", "Ohio". + upper = text.upper() + if len(upper) == 2 and upper in content.STATE_NAMES: + scope = state_scope(upper) + if scope: + return scope + code = content.STATE_CODES_BY_NAME.get(text.lower()) + if code: + scope = state_scope(code) + if scope: + return scope + + digits = re.sub(r"[^0-9]", "", text) + if len(digits) >= 5: + for store in stores: + if store.zip.replace("-", "").startswith(digits[:5]): + return {"kind": "store", "store": store, "label": store.city_state} + parts = [p.strip() for p in text.split(",") if p.strip()] + city = parts[0].lower() if parts else "" + state = parts[1].upper()[:2] if len(parts) > 1 else "" + if state: + for store in stores: + if store.city.lower() == city and store.state == state: + return {"kind": "store", "store": store, "label": store.city_state} + for store in stores: + if store.city.lower() == city: + return {"kind": "store", "store": store, "label": store.city_state} + return None + + +def current_filters() -> dict: + """Read and strictly validate the results-page query string.""" + q_value = single_query_arg("q") + search_value = single_query_arg("searchQuery") + if q_value and search_value: + abort(400, description="use q or searchQuery, not both") + query = (q_value or search_value).strip() + location = single_query_arg("loc").strip() + if len(query) > MAX_QUERY_LENGTH or len(location) > MAX_LOCATION_LENGTH: + abort(400, description="query text is too long") + if any(ord(char) < 32 for char in query + location): + abort(400, description="query contains unsupported control characters") + if query.casefold() == "all": + query = "" + if len(tokenize(query)) > 12: + abort(400, description="search contains too many terms") + + page_text = single_query_arg("page", "1") + radius_text = single_query_arg("radius", "25") + sort = single_query_arg("sort", "relevance") + tab = single_query_arg("tab", "jobs") + if not page_text.isdigit() or not 1 <= int(page_text) <= 10000: + abort(400, description="invalid page") + if not radius_text.isdigit() or int(radius_text) not in RADIUS_VALUES: + abort(400, description="invalid radius") + if sort not in SORT_VALUES or tab not in {"jobs", "future", "content"}: + abort(400, description="invalid sort or tab") + + area_values = {row.slug for row in Area.query.filter_by(is_filterable=True).all()} + category_values = {row.slug for row in Category.query.all()} + allowed = { + "area": area_values, + "category": category_values, + "brand": set(BRAND_VALUES), + "shift": set(SHIFT_VALUES), + "type": set(EMPLOYMENT_TYPE_VALUES), + "rate": set(RATE_VALUES), + } + facets: dict[str, list[str]] = {} + for name, choices in allowed.items(): + values = [value for value in request.args.getlist(name) if value] + if len(values) != len(set(values)) or any(value not in choices for value in values): + abort(400, description=f"invalid {name} filter") + facets[name] = values + return { + "q": query, + **facets, + "loc": location, + "radius": int(radius_text), + "sort": sort, + "page": int(page_text), + "tab": tab, + } + + +def filters_query(filters: dict, **overrides) -> str: + merged = dict(filters) + merged.update(overrides) + pairs: list[tuple[str, str]] = [] + if merged.get("q"): + pairs.append(("q", merged["q"])) + for key in ("area", "category", "brand", "shift", "type", "rate"): + for value in merged.get(key) or []: + pairs.append((key, value)) + if merged.get("loc"): + pairs.append(("loc", merged["loc"])) + pairs.append(("radius", str(merged.get("radius", 25)))) + if merged.get("sort") and merged["sort"] != "relevance": + pairs.append(("sort", merged["sort"])) + if merged.get("tab") and merged["tab"] != "jobs": + pairs.append(("tab", merged["tab"])) + if merged.get("page", 1) and int(merged.get("page", 1)) > 1: + pairs.append(("page", str(merged["page"]))) + return urlencode(pairs) + + +def _stem_match(token: str, blob_tokens: set[str]) -> bool: + """Loose prefix match for closely related words such as ``drivers`` and ``driver``. + + Two words match when they share a prefix of at least six characters and + their lengths are within three of each other. A five-letter prefix was + too loose: 'technician' matched 'Technology' and pulled half the catalog + into a title search. The search is scored, never a strict AND, so this + tier only widens a result set. + """ + if len(token) < 6: + return False + for other in blob_tokens: + if len(other) < 6 or abs(len(other) - len(token)) > 3: + continue + limit = min(len(token), len(other)) + shared = 0 + while shared < limit and token[shared] == other[shared]: + shared += 1 + if shared >= 6: + return True + return False + + +def score_job(job: Job, tokens: list[str]) -> float: + if not tokens: + return 0.0 + blob = job.search_blob.lower() + blob_tokens = {t for t in re.split(r"[^a-z0-9']+", blob) if t} + score = 0.0 + for token in tokens: + if token in blob_tokens: + score += 2.0 + elif len(token) >= 3 and any(other.startswith(token) for other in blob_tokens): + # word-prefix tier: 'cashi' -> cashier, 'hand' -> handler. A raw + # substring test let 'care' light up every Healthcare posting. + score += 1.0 + elif _stem_match(token, blob_tokens): + score += 0.5 + title_tokens = {t for t in re.split(r"[^a-z0-9']+", job.title.lower()) if t} + for token in tokens: + if token in title_tokens: + score += 1.5 + return score + + +def search_jobs(filters: dict) -> tuple[list[Job], dict | None, bool]: + """Return (ordered jobs, resolved location scope, location_failed).""" + jobs = Job.query.order_by(Job.job_id).all() + location = None + location_failed = False + if filters["loc"]: + location = resolve_location(filters["loc"]) + location_failed = location is None + if location_failed: + jobs = [] + + if filters["brand"]: + jobs = [j for j in jobs if j.brand in filters["brand"]] + if filters["type"]: + jobs = [j for j in jobs if j.employment_type in filters["type"]] + if filters["rate"]: + wanted = {"Salaried": "salaried", "Hourly": "hourly"} + allowed = {wanted[r] for r in filters["rate"]} + jobs = [j for j in jobs if j.population in allowed] + if filters["shift"]: + wanted_shifts = set(filters["shift"]) + jobs = [j for j in jobs if wanted_shifts & set(j.shifts)] + if filters["area"]: + wanted_areas = {a.lower() for a in filters["area"]} + jobs = [ + j for j in jobs + if j.area and (j.area.slug.lower() in wanted_areas or j.area.name.lower() in wanted_areas) + ] + if filters["category"]: + wanted_cats = {c.lower() for c in filters["category"]} + jobs = [ + j for j in jobs + if j.category and (j.category.slug.lower() in wanted_cats or j.category.name.lower() in wanted_cats) + ] + if location is not None: + if location["kind"] == "state": + jobs = [j for j in jobs if j.store.state == location["state"]] + else: + anchor = location["store"] + radius = filters["radius"] + jobs = [ + j for j in jobs + if haversine_miles(anchor.lat, anchor.lng, j.store.lat, j.store.lng) <= radius + ] + + tokens = tokenize(filters["q"]) + if tokens: + scored = [(score_job(j, tokens), j) for j in jobs] + scored = [(s, j) for s, j in scored if s > 0] + if filters["sort"] == "most_recent": + scored.sort(key=lambda pair: (-pair[1].posted_date.toordinal(), pair[1].sort_rank)) + else: + scored.sort(key=lambda pair: (-pair[0], pair[1].sort_rank, pair[1].job_id)) + jobs = [j for _, j in scored] + else: + if filters["sort"] == "most_recent": + jobs.sort(key=lambda j: (-j.posted_date.toordinal(), j.sort_rank)) + else: + jobs.sort(key=lambda j: (j.sort_rank, j.job_id)) + return jobs, location, location_failed + + +def cluster_map_svg(jobs: list[Job], width: int = 520, height: int = 620) -> str: + """Deterministic server-rendered cluster map (no third-party map tiles). + + Equirectangular with a cos(mean latitude) correction so the outline keeps a + believable shape, then centred vertically in the panel. + """ + lon_min, lon_max = -125.0, -65.0 + lat_min, lat_max = 17.0, 50.0 + pad = 12 + scale = (width - 2 * pad) / (lon_max - lon_min) + lat_scale = scale / math.cos(math.radians((lat_min + lat_max) / 2)) + y_offset = (height - (lat_max - lat_min) * lat_scale) / 2 + + def project(lat: float, lng: float) -> tuple[float, float]: + x = pad + (lng - lon_min) * scale + y = y_offset + (lat_max - lat) * lat_scale + return round(x, 1), round(y, 1) + + def path_for(points: list[tuple[float, float]]) -> str: + coords = [project(lat, lng) for lng, lat in points] + head = f"M {coords[0][0]} {coords[0][1]}" + rest = " ".join(f"L {x} {y}" for x, y in coords[1:]) + return f"{head} {rest} Z" + + counts: dict[int, int] = {} + for job in jobs: + counts[job.store_id] = counts.get(job.store_id, 0) + 1 + bubbles = [] + for store_id, count in sorted(counts.items(), key=lambda kv: (-kv[1], kv[0])): + store = db.session.get(Store, store_id) + if store is None: + continue + x, y = project(store.lat, store.lng) + radius = 12 + min(14, count * 2) + bubbles.append((x, y, radius, count, f"{store.city}, {store.state}")) + + parts = [ + f'', + f'', + f'', + f'', + ] + for x, y, radius, count, label in bubbles: + parts.append( + f'{label}: {count} open roles' + f'' + f'{count}' + ) + parts.append( + f'Map data ©2026 Walmart Careers mirror' + ) + parts.append("") + return "".join(parts) + + +def pin_card_svg(store: Store, width: int = 490, height: int = 230) -> str: + """Small deterministic SVG map card used beside the address on the detail page. + + Stands in for the Google Maps thumbnail on the live page: same palette, a road + grid seeded from the store's own coordinates, and a pin over the location. + """ + seed = int(abs(store.lat * 1000) + abs(store.lng * 1000)) % 97 + vx = 40 + (seed % 7) * 22 + vy = 60 + (seed % 5) * 18 + parts = [ + f'', + f'', + # water + f'', + # roads + f'', + f'', + f'', + f'', + ] + px, py = width / 2, height / 2 - 18 + parts.append( + f'' + f'' + f'' + f'' + ) + parts.append( + f'' + f'{store.city}' + ) + parts.append( + f'' + f'Map data ©2026 Walmart Careers mirror' + ) + parts.append("") + return "".join(parts) + + +def trending_jobs() -> list[Job]: + return Job.query.filter_by(is_trending=True).order_by(Job.job_id).all() + + +def related_jobs(job: Job, limit: int = 3) -> list[Job]: + rows = ( + Job.query.filter( + Job.category_id == job.category_id, + Job.job_id != job.job_id, + Job.store_id != job.store_id, + ) + .order_by(Job.sort_rank, Job.job_id) + .limit(limit) + .all() + ) + if len(rows) < limit: + extra = ( + Job.query.filter( + Job.area_id == job.area_id, + Job.job_id != job.job_id, + Job.store_id != job.store_id, + ~Job.job_id.in_([r.job_id for r in rows]), + ) + .order_by(Job.sort_rank, Job.job_id) + .limit(limit - len(rows)) + .all() + ) + rows = rows + extra + return rows + + +def saved_job_ids() -> set[str]: + if not current_user.is_authenticated: + return set() + return { + row.job_id + for row in SavedJob.query.filter_by(user_id=current_user.id).all() + } + + +def discard_application_draft() -> None: + token = session.pop("apply_draft_token", None) + if not token: + return + draft = ApplicationDraft.query.filter_by(token=token).first() + if draft is not None: + db.session.delete(draft) + db.session.commit() + + +@app.context_processor +def inject_globals(): + return { + # the six career areas listed in the header "Career areas" menu + "nav_areas": ( + Area.query.filter_by(is_filterable=True) + .order_by(Area.display_order) + .all() + ), + "content": content, + "current_year": content.MIRROR_REFERENCE_DATE.year, + "search_q": (request.args.get("q") or request.args.get("searchQuery") or ""), + } + + +# --------------------------------------------------------------------------- # +# Routes +# --------------------------------------------------------------------------- # +@app.route("/home") +@app.route("/") +def index(): + areas = ( + Area.query.filter_by(has_index_page=True, is_filterable=True) + .order_by(Area.display_order) + .all() + ) + return render_template( + "index.html", + trending=trending_jobs(), + ribbon_areas=areas, + saved_ids=saved_job_ids(), + ) + + +FACET_KEYS = ("area", "category", "brand", "shift", "type", "rate") + + +def active_filter_count(filters: dict) -> int: + """How many facet selections are active — the number on the Filters button.""" + return sum(len(filters[key]) for key in FACET_KEYS) + + +def active_filter_chips(filters: dict) -> list[dict]: + """One removable chip per active selection, so the current filter state stays + readable without leaving the Filters popover hanging open over the results.""" + names = {a.slug: a.name for a in Area.query.all()} + names.update({c.slug: c.name for c in Category.query.all()}) + chips: list[dict] = [] + for key in FACET_KEYS: + for value in filters[key]: + remaining = [v for v in filters[key] if v != value] + chips.append( + { + "label": names.get(value, value), + "remove": url_for("results") + + "?" + + filters_query(filters, page=1, **{key: remaining}), + } + ) + if filters["loc"]: + chips.append( + { + "label": f"{filters['loc']} · within {filters['radius']} miles", + "remove": url_for("results") + "?" + filters_query(filters, loc="", page=1), + } + ) + return chips + + +@app.route("/results") +def results(): + filters = current_filters() + jobs, location, location_failed = search_jobs(filters) + total = len(jobs) + pages = max(1, math.ceil(total / PAGE_SIZE)) + page = min(filters["page"], pages) + filters["page"] = page + start = (page - 1) * PAGE_SIZE + page_jobs = jobs[start:start + PAGE_SIZE] + + areas = Area.query.filter_by(is_filterable=True).order_by(Area.display_order).all() + return render_template( + "results.html", + filters=filters, + jobs=page_jobs, + total=total, + page=page, + pages=pages, + areas=areas, + shift_values=SHIFT_VALUES, + brand_values=BRAND_VALUES, + employment_type_values=EMPLOYMENT_TYPE_VALUES, + rate_values=RATE_VALUES, + radius_values=RADIUS_VALUES, + location=location, + location_failed=location_failed, + map_svg=cluster_map_svg(jobs), + saved_ids=saved_job_ids(), + qs=filters_query, + filter_count=active_filter_count(filters), + filter_chips=active_filter_chips(filters), + ) + + +@app.route("/jobs/") +def job_detail(job_id: str): + job = db.session.get(Job, job_id) + if job is None: + abort(404) + return render_template( + "job_detail.html", + job=job, + related=related_jobs(job), + is_saved=job.job_id in saved_job_ids(), + map_svg=pin_card_svg(job.store), + benefit_tiles=content.benefit_tiles_for(job.brand, job.population), + saved_ids=saved_job_ids(), + ) + + +@app.route("/jobs//save", methods=["POST"]) +def save_job(job_id: str): + job = db.session.get(Job, job_id) + if job is None: + abort(404) + if not current_user.is_authenticated: + return redirect(url_for("login", next=url_for("job_detail", job_id=job_id))) + existing = SavedJob.query.filter_by(user_id=current_user.id, job_id=job_id).first() + if existing is None: + db.session.add( + SavedJob(user_id=current_user.id, job_id=job_id, saved_at=datetime.now()) + ) + try: + db.session.commit() + except IntegrityError: + db.session.rollback() + else: + flash(f"Saved {job.title} to your saved roles.", "success") + target = safe_next(request.form.get("next")) or url_for("job_detail", job_id=job_id) + return redirect(target) + + +@app.route("/jobs//unsave", methods=["POST"]) +def unsave_job(job_id: str): + job = db.session.get(Job, job_id) + if job is None: + abort(404) + if not current_user.is_authenticated: + return redirect(url_for("login", next=url_for("saved_roles"))) + existing = SavedJob.query.filter_by(user_id=current_user.id, job_id=job_id).first() + if existing is not None: + db.session.delete(existing) + db.session.commit() + flash(f"Removed {job.title} from your saved roles.", "success") + target = safe_next(request.form.get("next")) or url_for("saved_roles") + return redirect(target) + + +@app.route("/jobs//apply", methods=["GET", "POST"]) +def apply_contact(job_id: str): + job = db.session.get(Job, job_id) + if job is None: + abort(404) + errors: list[str] = [] + form = { + "email": "", + "first_name": "", + "last_name": "", + "phone": "", + } + if current_user.is_authenticated: + form.update( + { + "email": current_user.email, + "first_name": current_user.first_name, + "last_name": current_user.last_name, + "phone": current_user.phone, + } + ) + if request.method == "POST": + limits = {"email": 160, "first_name": 80, "last_name": 80, "phone": 32} + for key, maximum in limits.items(): + form[key], error = bounded_text(request.form.get(key), key.replace("_", " "), maximum, required=True) + if error: + errors.append(error) + form["email"] = form["email"].lower() + agreed = request.form.get("terms") == "on" + if form["email"] and (not EMAIL_PATTERN.fullmatch(form["email"]) + or len(form["email"].split("@", 1)[0]) > 64): + errors.append("Enter a valid email address.") + phone_digits = re.sub(r"[^0-9]", "", form["phone"]) + if form["phone"] and not 10 <= len(phone_digits) <= 15: + errors.append("Enter a phone number with 10 to 15 digits.") + if not agreed: + errors.append("You must accept the Terms & Conditions to continue.") + if not errors: + discard_application_draft() + draft = ApplicationDraft( + token=secrets.token_urlsafe(32), + job_id=job_id, + user_id=current_user.id if current_user.is_authenticated else None, + email=form["email"], + first_name=form["first_name"], + last_name=form["last_name"], + phone=form["phone"], + created_at=datetime.now(), + ) + db.session.add(draft) + db.session.commit() + session["apply_draft_token"] = draft.token + return redirect(url_for("apply_confirm", job_id=job_id)) + return render_template("apply_contact.html", job=job, form=form, errors=errors) + + +@app.route("/jobs//apply/confirm", methods=["GET", "POST"]) +def apply_confirm(job_id: str): + job = db.session.get(Job, job_id) + if job is None: + abort(404) + token = session.get("apply_draft_token") + draft = ApplicationDraft.query.filter_by(token=token, job_id=job_id).first() if token else None + expected_user_id = current_user.id if current_user.is_authenticated else None + if draft is None or draft.user_id != expected_user_id: + discard_application_draft() + flash("Start your application by entering your contact details.", "warning") + return redirect(url_for("apply_contact", job_id=job_id)) + if request.method == "POST": + application = Application( + job_id=job_id, + user_id=current_user.id if current_user.is_authenticated else None, + email=draft.email, + first_name=draft.first_name, + last_name=draft.last_name, + phone=draft.phone, + status="Submitted", + confirmation_no="pending", + submitted_at=datetime.now(), + ) + db.session.add(application) + db.session.delete(draft) + try: + db.session.flush() + application.confirmation_no = confirmation_for(application.id) + db.session.commit() + except IntegrityError: + db.session.rollback() + abort(409, description="application could not be submitted") + session.pop("apply_draft_token", None) + session["apply_submitted_id"] = application.id + return redirect(url_for("apply_submitted", job_id=job_id)) + return render_template("apply_confirm.html", job=job, draft=draft) + + +@app.route("/jobs//apply/submitted") +def apply_submitted(job_id: str): + job = db.session.get(Job, job_id) + if job is None: + abort(404) + application_id = session.get("apply_submitted_id") + application = db.session.get(Application, application_id) if application_id else None + expected_user_id = current_user.id if current_user.is_authenticated else None + if application is None or application.job_id != job_id or application.user_id != expected_user_id: + flash("We couldn't find that application. Please apply again.", "warning") + return redirect(url_for("apply_contact", job_id=job_id)) + return render_template("apply_submitted.html", job=job, application=application) + + +@app.route("/careers-areas/") +def career_area(slug: str): + area = Area.query.filter(db.func.lower(Area.slug) == slug.lower()).first() + if area is None: + abort(404) + if not area.has_index_page: + # Students has no index page upstream either; send it to its open roles. + return redirect(url_for("results", area=area.slug)) + categories = ( + Category.query.filter_by(area_id=area.id) + .order_by(Category.display_order) + .all() + ) + return render_template( + "area.html", + area=area, + categories=categories, + hubs=Store.query.filter_by(is_hub=True).order_by(Store.id).all(), + ) + + +@app.route("/resources/location") +def resources_location(): + hubs = Store.query.filter_by(is_hub=True).order_by(Store.id).all() + return render_template("locations.html", hubs=hubs) + + +@app.route("/resources/hiring-process") +def resources_hiring(): + return render_template("hiring_process.html", trending=trending_jobs(), saved_ids=saved_job_ids()) + + +@app.route("/resources/terms-and-conditions") +def resources_terms(): + return render_template("terms.html") + + +@app.route("/about-us") +def about_us(): + return render_template( + "about.html", + areas=Area.query.filter_by(is_filterable=True).order_by(Area.display_order).all(), + ) + + +@app.route("/login", methods=["GET", "POST"]) +def login(): + next_url = safe_next(request.args.get("next")) + errors: list[str] = [] + email = "" + if request.method == "POST": + email, email_error = bounded_text(request.form.get("email"), "email", 160, required=True) + email = email.lower() + password = request.form.get("password") or "" + next_url = safe_next(request.form.get("next")) or next_url + if email_error or len(password) > MAX_PASSWORD_LENGTH: + errors.append("We couldn't sign you in with that email and password.") + user = None + else: + user = User.query.filter_by(email=email).first() + if user is None or not user.check_password(password): + if not errors: + errors.append("We couldn't sign you in with that email and password.") + else: + discard_application_draft() + session.pop("apply_submitted_id", None) + login_user(user) + flash(f"Signed in as {user.display_name}.", "success") + return redirect(next_url or url_for("index")) + return render_template("login.html", errors=errors, email=email, next_url=next_url) + + +@app.route("/register", methods=["GET", "POST"]) +def register(): + next_url = safe_next(request.args.get("next")) + errors: list[str] = [] + form = {"email": "", "first_name": "", "last_name": ""} + if request.method == "POST": + limits = {"email": 160, "first_name": 80, "last_name": 80} + for key, maximum in limits.items(): + form[key], error = bounded_text(request.form.get(key), key.replace("_", " "), maximum, required=True) + if error: + errors.append(error) + password = request.form.get("password") or "" + confirm = request.form.get("confirm_password") or "" + next_url = safe_next(request.form.get("next")) or next_url + email = form["email"].lower() + if form["email"] and (not EMAIL_PATTERN.fullmatch(email) + or len(email.split("@", 1)[0]) > 64): + errors.append("Enter a valid email address.") + elif User.query.filter_by(email=email).first(): + errors.append("An account already exists for that email address.") + if not 8 <= len(password) <= MAX_PASSWORD_LENGTH: + errors.append(f"Choose a password with 8 to {MAX_PASSWORD_LENGTH} characters.") + if password != confirm: + errors.append("The two passwords don't match.") + if not form["first_name"]: + errors.append("Enter your first name.") + if not form["last_name"]: + errors.append("Enter your last name.") + if not errors: + display = f"{form['first_name']} {form['last_name']}".strip() + base_username = email.split("@")[0] + username = base_username + suffix = 2 + while User.query.filter_by(username=username).first(): + username = f"{base_username}.{suffix}" + suffix += 1 + user = User( + email=email, + username=username, + display_name=display, + first_name=form["first_name"], + last_name=form["last_name"], + phone="", + city="", + state="", + password_hash=generate_password_hash(password), + created_at=datetime.now(), + ) + db.session.add(user) + try: + db.session.commit() + except IntegrityError: + db.session.rollback() + errors.append("That account identifier is already in use.") + return render_template("register.html", errors=errors, form=form, next_url=next_url), 409 + discard_application_draft() + session.pop("apply_submitted_id", None) + login_user(user) + flash("Your candidate account is ready.", "success") + return redirect(next_url or url_for("saved_roles")) + return render_template("register.html", errors=errors, form=form, next_url=next_url) + + +@app.route("/logout", methods=["POST"]) +@login_required +def logout(): + discard_application_draft() + logout_user() + session.pop("apply_submitted_id", None) + flash("You have been signed out.", "info") + return redirect(url_for("index")) + + +@app.route("/account") +@login_required +def account(): + return render_template( + "account.html", + saved_count=SavedJob.query.filter_by(user_id=current_user.id).count(), + application_count=Application.query.filter_by(user_id=current_user.id).count(), + ) + + +@app.route("/account/edit", methods=["GET", "POST"]) +@login_required +def account_edit(): + errors: list[str] = [] + form = { + "display_name": current_user.display_name, + "first_name": current_user.first_name, + "last_name": current_user.last_name, + "phone": current_user.phone, + "city": current_user.city, + "state": current_user.state, + } + if request.method == "POST": + limits = {"display_name": 120, "first_name": 80, "last_name": 80, + "phone": 32, "city": 80, "state": 2} + for key, maximum in limits.items(): + form[key], error = bounded_text(request.form.get(key), key.replace("_", " "), maximum, + required=key in {"display_name", "first_name", "last_name"}) + if error: + errors.append(error) + form["state"] = form["state"].upper() + phone_digits = re.sub(r"[^0-9]", "", form["phone"]) + if form["phone"] and not 10 <= len(phone_digits) <= 15: + errors.append("Enter a phone number with 10 to 15 digits.") + if form["state"] and form["state"] not in content.STATE_NAMES: + errors.append("Use a valid two-letter state or territory code.") + if not errors: + for key, value in form.items(): + setattr(current_user, key, value) + try: + db.session.commit() + except IntegrityError: + db.session.rollback() + errors.append("Your profile could not be updated.") + return render_template("account_edit.html", form=form, errors=errors), 409 + flash("Your profile has been updated.", "success") + return redirect(url_for("account")) + return render_template("account_edit.html", form=form, errors=errors) + + +@app.route("/candidate-home/saved-roles") +def saved_roles(): + rows: list[SavedJob] = [] + if current_user.is_authenticated: + rows = ( + SavedJob.query.filter_by(user_id=current_user.id) + .order_by(SavedJob.saved_at.desc(), SavedJob.id.desc()) + .all() + ) + return render_template( + "saved_roles.html", + rows=rows, + trending=trending_jobs(), + saved_ids=saved_job_ids(), + ) + + +@app.route("/candidate-home/applications") +@login_required +def applications(): + rows = ( + Application.query.filter_by(user_id=current_user.id) + .order_by(Application.submitted_at.desc(), Application.id.desc()) + .all() + ) + return render_template("applications.html", rows=rows) + + +@app.route("/_health") +def health(): + counts = { + "jobs": Job.query.count(), + "stores": Store.query.count(), + "areas": Area.query.count(), + "categories": Category.query.count(), + "users": User.query.count(), + } + marker = db.session.get(SeedMetadata, "version") + core_ready = {key: counts[key] for key in ("jobs", "stores", "areas", "categories")} == { + "jobs": 246, "stores": 51, "areas": 7, "categories": 33 + } + benchmark_users = {"alice.j@test.com", "bob.c@test.com", "carol.d@test.com", "david.k@test.com"} + present_users = {row.email for row in User.query.filter(User.email.in_(benchmark_users)).all()} + ready = core_ready and present_users == benchmark_users and marker is not None and marker.value == SEED_VERSION + return jsonify({"ok": ready, "site": "walmart_careers", "seed_version": marker.value if marker else None, **counts}), (200 if ready else 503) + + +@app.errorhandler(404) +def not_found(_error): + return render_template("404.html"), 404 + + +@app.errorhandler(500) +def server_error(_error): # pragma: no cover - defensive + db.session.rollback() + return render_template("500.html"), 500 + + +def bootstrap_site() -> None: + from seed_data import ensure_seed_database + + with app.app_context(): + db.create_all() + ensure_seed_database() + + +# `python app.py` loads this file as __main__; register it under its import +# name too so seed_data's `from app import ...` reuses this module instead of +# building a second Flask app + SQLAlchemy instance. +sys.modules.setdefault("app", sys.modules[__name__]) + +if os.environ.get("WEBSYN_SKIP_BOOTSTRAP") != "1": + bootstrap_site() + + +if __name__ == "__main__": + port = int(os.environ.get("PORT", "5000")) + app.run(host="0.0.0.0", port=port, debug=False) diff --git a/sites/walmart_careers/asset_inventory.json b/sites/walmart_careers/asset_inventory.json new file mode 100644 index 00000000..f9ca4350 --- /dev/null +++ b/sites/walmart_careers/asset_inventory.json @@ -0,0 +1,289 @@ +{ + "schema_version": 1, + "asset_count": 35, + "total_bytes": 11509127, + "direct_asset_urls": 17, + "source_page_only": 18, + "assets": [ + { + "path": "static/images/area-corporate-2.jpg", + "bytes": 251053, + "sha256": "95a904bb101b84d663f59de40cb70476d005a4da01acb12aa1d95004240d2e9d", + "source_url": "https://careers.walmart.com/us/en/home/careers-areas/corporate", + "source_kind": "source_page", + "source_evidence": "source page recorded by the contributor; exact original asset URL was not retained in PR head" + }, + { + "path": "static/images/area-corporate.jpg", + "bytes": 237324, + "sha256": "6ea665e0d1a7e090aac38c15feed83fbfacef1cb805d8da4b5b7889600ad241c", + "source_url": "https://careers.walmart.com/us/en/home/careers-areas/corporate", + "source_kind": "source_page", + "source_evidence": "source page recorded by the contributor; exact original asset URL was not retained in PR head" + }, + { + "path": "static/images/area-healthcare.jpg", + "bytes": 169606, + "sha256": "a22705a4980cd3f0fc4ee430563f30a38d3ef58f6d7f755e01d397a918757158", + "source_url": "https://careers.walmart.com/us/en/home/careers-areas/healthcare", + "source_kind": "source_page", + "source_evidence": "source page recorded by the contributor; exact original asset URL was not retained in PR head" + }, + { + "path": "static/images/area-military.jpg", + "bytes": 185880, + "sha256": "1c51e00e16608a46cc59fee8fe1a338e55882dabceb39f29fea450b7b6700839", + "source_url": "https://careers.walmart.com/us/en/home/careers-areas/military", + "source_kind": "source_page", + "source_evidence": "source page recorded by the contributor; exact original asset URL was not retained in PR head" + }, + { + "path": "static/images/area-stores-2.jpg", + "bytes": 329912, + "sha256": "c65a235874c0feb76717692e8cfd05177dbcbe61895721a1fd1a9b87e9f5d168", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/stores-l1/_DSC9230.jpg", + "source_kind": "direct_asset", + "source_evidence": "current live asset matched independently by perceptual comparison" + }, + { + "path": "static/images/area-stores-3.jpg", + "bytes": 182287, + "sha256": "7f62a00c73e1aa323bc80b1aae197a57f38f3268ed1cd24ebb688748411b7abe", + "source_url": "https://careers.walmart.com/us/en/home/careers-areas/stores-and-clubs", + "source_kind": "source_page", + "source_evidence": "source page recorded by the contributor; exact original asset URL was not retained in PR head" + }, + { + "path": "static/images/area-stores.jpg", + "bytes": 215513, + "sha256": "6a32c8b5463fee2c61f56d2f16aa9fbc534b8197a128c643eda90f962ed7782b", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/stores-l1/ILC_DM_20250206_1013.jpg", + "source_kind": "direct_asset", + "source_evidence": "current live asset matched independently by perceptual comparison" + }, + { + "path": "static/images/area-students.png", + "bytes": 1774223, + "sha256": "099dc378c06dbb0546fcad5e0b4af740120421da234bb3738f06ab6121f3c14d", + "source_url": "https://careers.walmart.com/us/en/home/careers-areas/students", + "source_kind": "source_page", + "source_evidence": "source page recorded by the contributor; exact original asset URL was not retained in PR head" + }, + { + "path": "static/images/area-supply-chain-2.jpg", + "bytes": 208558, + "sha256": "1616563a334326b5cdb6a4a3c59438ce9c22cc386e502beb150e0b775cf08671", + "source_url": "https://careers.walmart.com/us/en/home/careers-areas/supply-chain-and-transportation", + "source_kind": "source_page", + "source_evidence": "source page recorded by the contributor; exact original asset URL was not retained in PR head" + }, + { + "path": "static/images/area-supply-chain.jpg", + "bytes": 244329, + "sha256": "58b68f540552c4fac2c3efbe0a1902e1f29356ca597b42c7fb57522255c43119", + "source_url": "https://careers.walmart.com/us/en/home/careers-areas/supply-chain-and-transportation", + "source_kind": "source_page", + "source_evidence": "source page recorded by the contributor; exact original asset URL was not retained in PR head" + }, + { + "path": "static/images/area-technology-2.jpg", + "bytes": 33405, + "sha256": "eb26e30dbc1b198d195ecffea161973db326fc5db4a12e8862ff772e56d0c649", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/global-tech/Walmart%20-%20Global%20Tech%20-%20Sunnyvale_0001.jpg", + "source_kind": "direct_asset", + "source_evidence": "current live asset matched independently by perceptual comparison" + }, + { + "path": "static/images/area-technology.jpg", + "bytes": 122001, + "sha256": "f128142e1d48ee7958c124b744cde6e02bad554a13c4e5ac63b8623be1ab8fa0", + "source_url": "https://careers.walmart.com/us/en/home/careers-areas/technology", + "source_kind": "source_page", + "source_evidence": "source page recorded by the contributor; exact original asset URL was not retained in PR head" + }, + { + "path": "static/images/home-carousel-1.jpg", + "bytes": 149266, + "sha256": "07479403c7e1b0d940d3b466e6702b4d8d4cf53c7931251967795fd442177226", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/home/Adobe%20Express%20-%20file.jpg", + "source_kind": "direct_asset", + "source_evidence": "current live asset matched independently by perceptual comparison" + }, + { + "path": "static/images/home-hero.jpg", + "bytes": 151077, + "sha256": "0a17d5d0c75da0c3b6c4e41b7839731c943da6a69e62f00a19142f576503758c", + "source_url": "https://careers.walmart.com/us/en/home", + "source_kind": "source_page", + "source_evidence": "source page recorded by the contributor; exact original asset URL was not retained in PR head" + }, + { + "path": "static/images/home-milestone.jpg", + "bytes": 175232, + "sha256": "3afc35721645dff131688697edeb79663eddb40229709f41253cfea2a0886713", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/home/milestone-image-2.jpg", + "source_kind": "direct_asset", + "source_evidence": "current live asset matched independently by perceptual comparison" + }, + { + "path": "static/images/jobhero-corp-1.jpg", + "bytes": 125219, + "sha256": "8148264bd8c83cd3d209454dde9c81df668e1470b9c7871d62992c40802441da", + "source_url": "https://careers.walmart.com/us/en/home/careers-areas/technology", + "source_kind": "source_page", + "source_evidence": "source page recorded by the contributor; exact original asset URL was not retained in PR head" + }, + { + "path": "static/images/jobhero-corp-2.jpg", + "bytes": 211139, + "sha256": "ccd78a7c2e3ac03f35aec833c6aba3755fcb5251273747580f20beca079b4adc", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/technology-l2/software-engineering-and-architecture/Global%20TechRetouched-34.jpg", + "source_kind": "direct_asset", + "source_evidence": "current live asset matched independently by perceptual comparison" + }, + { + "path": "static/images/jobhero-corp-3.jpg", + "bytes": 265973, + "sha256": "c53546a2627b1acee8ae6b4fbdd8a4a66eb6bdd5dc6ab84b94b608ff19872b87", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/corporate-l2/Walmart%20Connect%20-%20San%20Bruno%20Office-29.jpg", + "source_kind": "direct_asset", + "source_evidence": "current live asset matched independently by perceptual comparison" + }, + { + "path": "static/images/jobhero-sams-1.png", + "bytes": 786967, + "sha256": "58a7a61cc690f1979d3488a15c83433f3dba2e990ff882b359537236214d3405", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/home/values-store-girl.jpg", + "source_kind": "direct_asset", + "source_evidence": "current live asset matched independently by perceptual comparison" + }, + { + "path": "static/images/jobhero-sams-2.jpg", + "bytes": 412819, + "sha256": "1054ac12ac61a81869dc3a332ac043c99b2588d017199f8d02a60553eafb67c9", + "source_url": "https://careers.walmart.com/us/en/home", + "source_kind": "source_page", + "source_evidence": "source page recorded by the contributor; exact original asset URL was not retained in PR head" + }, + { + "path": "static/images/jobhero-wm-1.png", + "bytes": 568017, + "sha256": "53b5ab4571efea9084f685e21f89b96d77caae9078a7aa58a827b0128c2e9c37", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/home/jobdetails-hero-wm-field-3.png", + "source_kind": "direct_asset", + "source_evidence": "current live asset matched independently by perceptual comparison" + }, + { + "path": "static/images/jobhero-wm-2.jpg", + "bytes": 282247, + "sha256": "501396e96f2b147eff92711e6d957ad04422a3fd1a0e0e454497225f2312be93", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/stores-l1/ILC_DM_20250508_3742.jpg", + "source_kind": "direct_asset", + "source_evidence": "current live asset matched independently by perceptual comparison" + }, + { + "path": "static/images/jobhero-wm-3.jpg", + "bytes": 231129, + "sha256": "590b203dd25302f7cc269da820542b8b15bbfb83927fc19c59bfe578f4b4c234", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/stores-l1/ILC_DM_20250205_0390.jpg", + "source_kind": "direct_asset", + "source_evidence": "current live asset matched independently by perceptual comparison" + }, + { + "path": "static/images/jobhero-wm-4.jpg", + "bytes": 338873, + "sha256": "b61a0b746a2d2ab8221060e036f6e7a705acc0a090b9a240f596ea13d90c9fe2", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/job-description-pages/5522263-FY26-NEB-Store-4108-PR-Images_AS21533-Version-3.jpg", + "source_kind": "direct_asset", + "source_evidence": "current live asset matched independently by perceptual comparison" + }, + { + "path": "static/images/life-associates.jpg", + "bytes": 323053, + "sha256": "1d718b2cb02566fcdf374220361abce3f9ed190dbfbf667b889fa9e3183eb1af", + "source_url": "https://careers.walmart.com/us/en/about-us", + "source_kind": "source_page", + "source_evidence": "source page recorded by the contributor; exact original asset URL was not retained in PR head" + }, + { + "path": "static/images/life-home-office.jpg", + "bytes": 263939, + "sha256": "2079d48524ff466b65e02ad45029c94ae300f9f0bd61b062a223c05d49c103c5", + "source_url": "https://careers.walmart.com/us/en/about-us", + "source_kind": "source_page", + "source_evidence": "source page recorded by the contributor; exact original asset URL was not retained in PR head" + }, + { + "path": "static/images/loc-dallas.jpg", + "bytes": 103715, + "sha256": "f43ce6914397a28ef97a59e279bfe9a5dd49ebdc28029bcf93418326081d7d83", + "source_url": "https://careers.walmart.com/us/en/resources/location", + "source_kind": "source_page", + "source_evidence": "source page recorded by the contributor; exact original asset URL was not retained in PR head" + }, + { + "path": "static/images/loc-hoboken.jpg", + "bytes": 404878, + "sha256": "e159be4150f18bb0d1c96786e0cd646780e378180c37a4a7ddf00e1c8305f38b", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/areas/technology/hoboken.jpg", + "source_kind": "direct_asset", + "source_evidence": "current live asset matched independently by perceptual comparison" + }, + { + "path": "static/images/loc-international.jpg", + "bytes": 97990, + "sha256": "34a9c0d0daf0ee18470e4b9fe3162020f3b2f1b599b26437d20fd28450e99f39", + "source_url": "https://careers.walmart.com/us/en/resources/location", + "source_kind": "source_page", + "source_evidence": "source page recorded by the contributor; exact original asset URL was not retained in PR head" + }, + { + "path": "static/images/loc-nwa.jpg", + "bytes": 411964, + "sha256": "b055b162de03d41ceb50d14febbf59845c59881b476fca8be9b4561177c025b0", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/areas/technology/northwest-arkansas.jpg", + "source_kind": "direct_asset", + "source_evidence": "current live asset matched independently by perceptual comparison" + }, + { + "path": "static/images/loc-silicon-valley.jpg", + "bytes": 383994, + "sha256": "1937268ee3d2803580dccd88e3a9d3605f02bb18aa027a2acd6b9590b3a9f859", + "source_url": "https://careers.walmart.com/us/en/resources/location", + "source_kind": "source_page", + "source_evidence": "source page recorded by the contributor; exact original asset URL was not retained in PR head" + }, + { + "path": "static/images/loc-sunnyvale.jpg", + "bytes": 382937, + "sha256": "996711038e15cc7e27dd1e683dd701fdf93d16106e159a7c440d7f0fcd0c65c3", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/locations/Sunnyvale.jpg", + "source_kind": "direct_asset", + "source_evidence": "current live asset matched independently by perceptual comparison" + }, + { + "path": "static/images/military-banner.png", + "bytes": 1155379, + "sha256": "1b7cdd5fc3396905efe89f65b0822b6797a0956c9e93f8e6ae755cba68fc684f", + "source_url": "https://careers.walmart.com/us/en/home/careers-areas/military", + "source_kind": "source_page", + "source_evidence": "source page recorded by the contributor; exact original asset URL was not retained in PR head" + }, + { + "path": "static/images/supply-drone.jpg", + "bytes": 79504, + "sha256": "35560819fcb6f71a561dcd620963ca42ce21d0fea877ef3ef059ef110ee9a873", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/areas/technology/drone.jpg", + "source_kind": "direct_asset", + "source_evidence": "current live asset matched independently by perceptual comparison" + }, + { + "path": "static/images/testimonial-1.jpg", + "bytes": 249725, + "sha256": "35ef3587f6cd4d04f21cd458a6604aedb80e74c424300f1ad6b26812a9d57798", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/Testimonial%20Employee%20Images/IMG_3244-Edit.jpg", + "source_kind": "direct_asset", + "source_evidence": "current live asset matched independently by perceptual comparison" + } + ] +} diff --git a/sites/walmart_careers/catalog_source.py b/sites/walmart_careers/catalog_source.py new file mode 100644 index 00000000..6cbb4134 --- /dev/null +++ b/sites/walmart_careers/catalog_source.py @@ -0,0 +1,2147 @@ +"""Deterministic source catalog for the Walmart Careers mirror. + +Every posting in the mirror is generated from this file: title families with an +explicit list of placements (store, employment type, shifts, pay band, open +positions). Body copy comes from per-family templates with slot fills, so the +200 postings stay internally consistent without 200 hand-written essays. + +Nothing here is read at request time. `seed_data.py` turns it into SQLite rows. +""" +from __future__ import annotations + +# --------------------------------------------------------------------------- # +# Areas +# --------------------------------------------------------------------------- # +# (slug, name, display_order, blurb, hero_image, has_index_page, is_filterable) +AREAS = [ + ( + "stores-and-clubs", + "Stores and Clubs", + 1, + "Find your path with us. Whether you're interested in auto care, front-end services, " + "general merchandising, or another team, you'll find opportunities to grow and the " + "support to reach your career goals.", + "area-stores.jpg", + True, + True, + ), + ( + "supply-chain-and-transportation", + "Supply Chain and Transportation", + 2, + "Move product, move people forward. Our distribution centers, fulfillment centers and " + "private fleet keep shelves stocked and orders on time across the country.", + "area-supply-chain.jpg", + True, + True, + ), + ( + "healthcare", + "Healthcare", + 3, + "Care that reaches everyone. Our pharmacy, vision and wellness teams serve millions of " + "neighbours every week, right inside the stores they already shop.", + "area-healthcare.jpg", + True, + True, + ), + ( + "technology", + "Technology", + 4, + "At Walmart and Sam's Club, we are people-led, tech-powered. Everything you build here - " + "from smarter supply chains to seamless shopping - starts with real needs and creates " + "real impact.", + "area-technology.jpg", + True, + True, + ), + ( + "corporate", + "Corporate", + 5, + "Strategy, finance, merchandising, marketing and people teams that set the direction for " + "the world's largest retailer.", + "area-corporate.jpg", + True, + True, + ), + ( + "students", + "Students", + 6, + "Internships and early career programs across every part of the business.", + "area-students.png", + False, + True, + ), + ( + "Military", + "Military", + 7, + "Your service prepared you to lead. Bring that experience to a company that hires " + "thousands of veterans, transitioning service members and military spouses every year.", + "area-military.jpg", + True, + False, + ), +] + +# (area_slug, category_name, category_slug, display_order) +CATEGORIES = [ + ("stores-and-clubs", "Cashier and Front-End Services", "cashier-and-front-end-services", 1), + ("stores-and-clubs", "Food and Grocery", "food-and-grocery", 2), + ("stores-and-clubs", "General Merchandise, Stocking, and Unloading", "general-merchandise-stocking-and-unloading", 3), + ("stores-and-clubs", "Digital Pickup and Delivery", "digital-pickup-and-delivery", 4), + ("stores-and-clubs", "Cafe", "cafe", 5), + ("stores-and-clubs", "Retail Management", "retail-management", 6), + ("stores-and-clubs", "Fuel Station", "fuel-station", 7), + ("stores-and-clubs", "Auto Care Center", "auto-care-center", 8), + ("stores-and-clubs", "Auto Services", "auto-services", 9), + ("stores-and-clubs", "Maintenance", "maintenance", 10), + ("stores-and-clubs", "Security and Asset Protection", "security-and-asset-protection", 11), + ("supply-chain-and-transportation", "SC&T Operations", "sct-operations", 1), + ("supply-chain-and-transportation", "Drivers", "drivers", 2), + ("supply-chain-and-transportation", "Engineering", "engineering", 3), + ("supply-chain-and-transportation", "Aviation", "aviation", 4), + ("supply-chain-and-transportation", "Security and Asset Protection", "sct-security-and-asset-protection", 5), + ("healthcare", "Pharmacy Services", "pharmacy-services", 1), + ("healthcare", "Optical Services", "optical-services", 2), + ("healthcare", "Health and Wellness Operations", "health-and-wellness-operations", 3), + ("healthcare", "Clinical Care", "clinical-care", 4), + ("technology", "Software Engineering and Architecture", "software-engineering-and-architecture", 1), + ("technology", "Product Management", "product-management", 2), + ("technology", "Data Science and Analytics", "data-science-and-analytics", 3), + ("technology", "Information Security", "information-security", 4), + ("technology", "Creative Design and UX", "creative-design-and-ux", 5), + ("technology", "Technical Program Management", "technical-program-management", 6), + ("technology", "Information Technology", "information-technology", 7), + ("corporate", "Accounting and Finance", "accounting-and-finance", 1), + ("corporate", "Human Resources", "human-resources", 2), + ("corporate", "Marketing and Advertising", "marketing-and-advertising", 3), + ("corporate", "Merchandising", "merchandising", 4), + ("corporate", "Business Operations", "business-operations", 5), + ("students", "Internship", "internship", 1), +] + +# --------------------------------------------------------------------------- # +# Stores (store_number, banner, location_name, street, city, state, zip, +# lat, lng, is_hub, is_office, brand) +# --------------------------------------------------------------------------- # +STORES = [ + # --- offices ----------------------------------------------------------- + ("10101", "Home Office", "WALMART HOME OFFICE", "702 SW 8th St", "Bentonville", "AR", "72716-0000", 36.363430, -94.219970, True, True, "Walmart"), + ("11807", "Home Office", "SUNNYVALE TECH CORNERS BLDG 6", "811 11th Ave", "Sunnyvale", "CA", "94089-4731", 37.402869, -122.036132, True, True, "Walmart"), + ("11003", "Home Office", "HOBOKEN TECH HUB", "221 River St", "Hoboken", "NJ", "07030-5989", 40.735657, -74.030324, True, True, "Walmart"), + ("11500", "Home Office", "DALLAS METRO OFFICE", "603 Munger Ave", "Dallas", "TX", "75202-3505", 32.784618, -96.796851, True, True, "Walmart"), + ("11109", "Home Office", "SAM'S CLUB HOME OFFICE", "2101 SE Simple Savings Dr", "Bentonville", "AR", "72712-4304", 36.343100, -94.196000, False, True, "Sam's Club"), + ("12200", "Vizio Campus", "VIZIO IRVINE CAMPUS", "39 Tesla", "Irvine", "CA", "92618-4603", 33.650800, -117.744400, False, True, "Vizio"), + # --- Arkansas ---------------------------------------------------------- + ("5260", "Neighborhood Market", "WM NEIGHBORHOOD MARKET #5260", "1400 SE Walton Blvd", "Bentonville", "AR", "72712-6220", 36.354900, -94.202500, False, False, "Walmart"), + ("144", "WM Supercenter", "WM SUPERCENTER #144", "2110 W Walnut St", "Rogers", "AR", "72756-3611", 36.334100, -94.152800, False, False, "Walmart"), + ("8259", "Sam's Club", "SAM'S CLUB #8259", "1101 SE Walton Blvd", "Bentonville", "AR", "72712-6191", 36.357800, -94.199200, False, False, "Sam's Club"), + ("8155", "Sam's Club", "SAM'S CLUB #8155", "3081 N College Ave", "Fayetteville", "AR", "72703-5100", 36.101000, -94.158000, False, False, "Sam's Club"), + # --- California -------------------------------------------------------- + ("9054", "eComm Whse Logistics", "ECOMM WHSE LOGISTICS #9054", "1290 W Henderson Ave", "Porterville", "CA", "93257-5969", 36.070300, -119.041800, False, False, "Walmart"), + ("2050", "WM Supercenter", "WM SUPERCENTER #2050", "3680 W Shaw Ave", "Fresno", "CA", "93711-3204", 36.808900, -119.828600, False, False, "Walmart"), + ("6608", "Sam's Club", "SAM'S CLUB #6608", "5205 Monterey Hwy", "San Jose", "CA", "95111-4106", 37.259700, -121.816200, False, False, "Sam's Club"), + # --- New Jersey -------------------------------------------------------- + ("2110", "WM Supercenter", "WM SUPERCENTER #2110", "400 Park Plaza Dr", "Secaucus", "NJ", "07094-3661", 40.786600, -74.061800, False, False, "Walmart"), + ("3520", "Neighborhood Market", "WM NEIGHBORHOOD MARKET #3520", "2100 88th St", "North Bergen", "NJ", "07047-4720", 40.792400, -74.011200, False, False, "Walmart"), + # --- Texas ------------------------------------------------------------- + ("9399", "eComm Whse Logistics", "ECOMM WHSE LOGISTICS #9399", "3401 Quincy St", "Plainview", "TX", "79072-3308", 34.164300, -101.700900, False, False, "Walmart"), + ("4750", "Sam's Club", "SAM'S CLUB #4750", "3000 E Plano Pkwy", "Plano", "TX", "75074-7440", 33.017200, -96.671900, False, False, "Sam's Club"), + ("471", "WM Supercenter", "WM SUPERCENTER #471", "4215 Canyon Dr", "Amarillo", "TX", "79110-1109", 35.166900, -101.850700, False, False, "Walmart"), + ("3826", "Neighborhood Market", "WM NEIGHBORHOOD MARKET #3826", "1521 N Cockrell Hill Rd", "Dallas", "TX", "75211-7407", 32.779000, -96.887000, False, False, "Walmart"), + # --- Florida ----------------------------------------------------------- + ("3387", "WM Supercenter", "WM SUPERCENTER #3387", "17000 Toledo Blade Blvd", "North Port", "FL", "34287-7281", 27.056300, -82.183200, False, False, "Walmart"), + ("6318", "Sam's Club", "SAM'S CLUB #6318", "4763 Millenia Plaza Way", "Orlando", "FL", "32839-6014", 28.485600, -81.430200, False, False, "Sam's Club"), + ("7133", "Regional DC", "REGIONAL DISTRIBUTION CENTER #7133", "3001 Bartow Rd", "Lakeland", "FL", "33803-6413", 27.981100, -81.930100, False, False, "Walmart"), + # --- Ohio -------------------------------------------------------------- + ("2073", "WM Supercenter", "WM SUPERCENTER #2073", "10000 Brookpark Rd", "Cleveland", "OH", "44130-1102", 41.409300, -81.786600, False, False, "Walmart"), + ("5388", "Neighborhood Market", "WM NEIGHBORHOOD MARKET #5388", "6594 Ridge Rd", "Parma", "OH", "44129-5546", 41.387000, -81.748000, False, False, "Walmart"), + ("6636", "Sam's Club", "SAM'S CLUB #6636", "3950 W Dublin Granville Rd", "Columbus", "OH", "43235-2701", 40.098700, -83.083100, False, False, "Sam's Club"), + ("5439", "Neighborhood Market", "WM NEIGHBORHOOD MARKET #5439", "5821 W Central Ave", "Toledo", "OH", "43615-2159", 41.673900, -83.673400, False, False, "Walmart"), + ("2075", "WM Supercenter", "WM SUPERCENTER #2075", "8585 Pearl Rd", "Strongsville", "OH", "44136-1618", 41.314000, -81.829000, False, False, "Walmart"), + ("5133", "WM Supercenter", "WM SUPERCENTER #5133", "24801 Brookpark Rd", "North Olmsted", "OH", "44070-3407", 41.429000, -81.916000, False, False, "Walmart"), + ("4744", "Sam's Club", "SAM'S CLUB #4744", "3560 Steelyard Dr", "Cleveland", "OH", "44109-2101", 41.458600, -81.688900, False, False, "Sam's Club"), + # --- New York ---------------------------------------------------------- + ("9046", "eComm Whse Logistics", "ECOMM WHSE LOGISTICS #9046", "8827 Old River Rd", "Marcy", "NY", "13403-3030", 43.173965, -75.315183, False, False, "Walmart"), + ("6038", "Regional DC", "REGIONAL DISTRIBUTION CENTER #6038", "5000 Halsey Rd", "Marcy", "NY", "13403-2317", 43.155900, -75.297400, False, False, "Walmart"), + ("2163", "WM Supercenter", "WM SUPERCENTER #2163", "1490 Hudson Ave", "Rochester", "NY", "14621-2404", 43.201800, -77.586300, False, False, "Walmart"), + # --- Kansas ------------------------------------------------------------ + ("5991", "Neighborhood Market", "WM NEIGHBORHOOD MARKET #5991", "2441 S Rock Rd", "Wichita", "KS", "67207-3254", 37.653900, -97.240100, False, False, "Walmart"), + ("1179", "WM Supercenter", "WM SUPERCENTER #1179", "1301 SW Wanamaker Rd", "Topeka", "KS", "66604-3843", 39.032200, -95.762700, False, False, "Walmart"), + ("6014", "Regional DC", "REGIONAL DISTRIBUTION CENTER #6014", "2101 S Princeton St", "Ottawa", "KS", "66067-8501", 38.588600, -95.263700, False, False, "Walmart"), + # --- Mississippi ------------------------------------------------------- + ("954", "WM Supercenter", "WM SUPERCENTER #954", "1266 Highway 51 N", "Hazlehurst", "MS", "39083-2217", 31.879400, -90.397300, False, False, "Walmart"), + ("1230", "WM Supercenter", "WM SUPERCENTER #1230", "1130 Brookway Blvd", "Brookhaven", "MS", "39601-3211", 31.556600, -90.443800, False, False, "Walmart"), + ("8253", "Sam's Club", "SAM'S CLUB #8253", "6360 Ridgewood Ct Dr", "Jackson", "MS", "39211-3520", 32.393200, -90.140800, False, False, "Sam's Club"), + # --- Iowa -------------------------------------------------------------- + ("9281", "eComm Whse Logistics", "ECOMM WHSE LOGISTICS #9281", "2600 Iris Rd", "Mount Pleasant", "IA", "52641-3106", 40.966400, -91.549600, False, False, "Walmart"), + ("1236", "WM Supercenter", "WM SUPERCENTER #1236", "5101 SE 14th St", "Des Moines", "IA", "50320-2201", 41.531700, -93.596800, False, False, "Walmart"), + # --- Washington -------------------------------------------------------- + ("4137", "WM Supercenter", "WM SUPERCENTER #4137", "1965 S Union Ave", "Tacoma", "WA", "98405-1615", 47.242300, -122.484500, False, False, "Walmart"), + ("6216", "Sam's Club", "SAM'S CLUB #6216", "9950 N Newport Hwy", "Spokane", "WA", "99218-1240", 47.741400, -117.400600, False, False, "Sam's Club"), + ("5382", "Neighborhood Market", "WM NEIGHBORHOOD MARKET #5382", "8102 Evergreen Way", "Everett", "WA", "98203-6428", 47.905400, -122.229900, False, False, "Walmart"), + ("7021", "Regional DC", "REGIONAL DISTRIBUTION CENTER #7021", "1300 Wine Country Rd", "Grandview", "WA", "98930-9704", 46.254000, -119.901000, False, False, "Walmart"), + # --- Puerto Rico ------------------------------------------------------- + ("2503", "WM Supercenter", "WM SUPERCENTER #2503", "Carr 2 KM 11.4", "Bayamon", "PR", "00959-5100", 18.394200, -66.155300, False, False, "Walmart"), + ("2610", "WM Supercenter", "WM SUPERCENTER #2610", "500 Ave Rafael Cordero", "Caguas", "PR", "00725-3607", 18.245600, -66.036200, False, False, "Walmart"), + ("3512", "Neighborhood Market", "WM NEIGHBORHOOD MARKET #3512", "2000 Ave Las Americas", "Ponce", "PR", "00717-0777", 18.019800, -66.612600, False, False, "Walmart"), + ("8763", "Sam's Club", "SAM'S CLUB #8763", "100 Ave Fragoso", "Carolina", "PR", "00979-1234", 18.417400, -65.977300, False, False, "Sam's Club"), + ("3593", "Neighborhood Market", "WM NEIGHBORHOOD MARKET #3593", "65 Ave De Diego", "San Juan", "PR", "00927-3300", 18.398500, -66.055300, False, False, "Walmart"), + # --- Virginia ---------------------------------------------------------- + ("1399", "WM Supercenter", "WM SUPERCENTER #1399", "1123 E Lynchburg Salem Tpke", "Bedford", "VA", "24523-3446", 37.323200, -79.502400, False, False, "Walmart"), + ("6088", "Import", "IMPORT DISTRIBUTION CENTER #6088", "8109 Merrimac Trail", "Williamsburg", "VA", "23185-6255", 37.288600, -76.664900, False, False, "Walmart"), +] + +# --------------------------------------------------------------------------- # +# Shifts +# --------------------------------------------------------------------------- # +SHIFT_CODES = { + "WD": "Weekday Day", + "WE": "Weekday Evening", + "WN": "Weekday Overnight", + "SD": "Weekend Day", + "SE": "Weekend Evening", + "SN": "Weekend Overnight", + "FX": "Flex", +} +SHIFT_WINDOWS = { + "WD": "Shift may start between 6:00am - 11:00am", + "WE": "Shift may start between 12:00pm - 5:00pm", + "WN": "Shift may start between 8:00pm - 1:00am", + "SD": "Shift may start between 5:00am - 10:00am", + "SE": "Shift may start between 1:00pm - 6:00pm", + "SN": "Shift may start between 6:00pm - 3:00am", + "FX": "Shift may start between 7:00am - 7:00pm", +} + + +# --------------------------------------------------------------------------- # +# Hourly title families. +# +# placement tuple: (store_number, employment_type, shift_codes, min_pay, +# max_pay, positions_available, extras) +# `extras` is an optional dict: {"job_id": ..., "shift_time": ..., "min_age": bool} +# Body copy is a per-family template; slots are {banner} {store} {city} {state}. +# --------------------------------------------------------------------------- # +HOURLY_FAMILIES = [ + { + "title": "Freight Handler", + "area": "supply-chain-and-transportation", + "category": "SC&T Operations", + "hashtag": "#freighthandlerjobs", + "summary": "Career opportunities in Freight Handling roles include Receiving, Unloading, " + "Processing, Orderfilling and Shipping.", + "do": [ + "As a Freight Handler at {banner} #{store} in {city}, {state}, you will have a critical role " + "in moving product through our supply chain network to the stores that serve our customers. " + "Your role is critical in providing our customers with the product they expect at an everyday " + "low price.", + "You can expect the work to be very physically demanding with an extremely high focus on your " + "safety and the safety of others. You will be lifting heavy cases in a climate-controlled and, " + "at times, non-climate-controlled environment. The flow of freight is very fast-paced and " + "productivity expectations are high.", + ], + "bring": [ + "Unload, sort and stage inbound freight using powered industrial equipment after certification.", + "Scan and verify case counts against the trailer manifest and flag discrepancies to the area coach.", + "Maintain a clean and safe work area, following all lockout/tagout and PPE requirements.", + "Complies with company policies, procedures, and standards of ethics and integrity. Performs " + "additional duties as assigned.", + ], + "placements": [ + ("9054", "Full time", "SN", 21.80, 25.30, 3, {"shift_time": "Shift may start between 6:00pm - 3:00am"}), + ("9399", "Part time", "SE", 18.50, 22.00, 2, None), + ("9046", "Full time", "WN", 20.90, 24.40, 4, {"shift_time": "Shift may start between 9:00pm - 1:30am"}), + ("6038", "Full time", "WE", 19.75, 23.25, 2, {"shift_time": "Shift may start between 3:00pm - 7:30pm"}), + ("9281", "Part time", "WD", 18.90, 22.40, 3, None), + ("6014", "Part time", "SD", 19.40, 22.90, 5, None), + ("7021", "Full time", "WN", 20.50, 24.00, 3, None), + ], + }, + { + "title": "eCom Warehouse Worker", + "area": "supply-chain-and-transportation", + "category": "SC&T Operations", + "hashtag": "#ecomwarehousejobs", + "summary": "Pick, pack and ship the online orders that customers are waiting on, inside one of " + "our fulfillment buildings.", + "do": [ + "As an eCom Warehouse Worker at {banner} #{store} in {city}, {state}, you pick customer orders " + "from bins and totes, pack them to our quality standard, and hand them to the outbound dock so " + "they ship the same night.", + "Expect a fast, metrics-driven floor. You will stand and walk for most of your shift, lift up to " + "50 pounds, and rotate across pick, pack and ship stations as volume moves.", + ], + "bring": [ + "Pick and pack customer orders to the published units-per-hour standard.", + "Use a handheld scanner and the warehouse management system to confirm every unit.", + "Report damaged product and inventory discrepancies before the order leaves the building.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("9046", "Part time", "SN", 21.35, 24.85, 2, {"job_id": "CP-9046-11274", "shift_time": "Shift may start between 6:00pm - 2:30am"}), + ("9054", "Part time", "WN", 20.60, 24.10, 1, None), + ("9281", "Part time", "SD,WN", 19.20, 22.70, 3, None), + ("7133", "Full time", "WD", 18.80, 22.30, 4, None), + ], + }, + { + "title": "Order Filler", + "area": "supply-chain-and-transportation", + "category": "SC&T Operations", + "hashtag": "#orderfillerjobs", + "summary": "Build store-ready pallets from the pick line and stage them for the outbound fleet.", + "do": [ + "Order Fillers at {banner} #{store} in {city}, {state} select cases from the pick line, build " + "stable pallets to the store's plan-o-gram sequence, wrap them, and stage them at the outbound " + "door for the driver.", + "You will use a rider pallet jack and a voice-directed pick system. Accuracy targets and case " + "rates are published daily and reviewed with your coach each week.", + ], + "bring": [ + "Select cases accurately using a voice-directed picking headset.", + "Build and wrap pallets that travel safely without shifting.", + "Operate a rider pallet jack after completing on-site certification.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("6014", "Full time", "WN,FX,SN", 19.10, 22.60, 4, None), + ("7133", "Part time", "SE,FX", 18.40, 21.90, 2, None), + ("6038", "Part time", "WD,FX", 20.10, 23.60, 3, None), + ("9399", "Full time", "SN,FX", 19.85, 23.35, 2, None), + ("7021", "Part time", "SD,FX", 19.30, 22.80, 2, None), + ], + }, + { + "title": "Yard Driver-Off Property", + "area": "supply-chain-and-transportation", + "category": "Drivers", + "hashtag": "#yarddriverjobs", + "summary": "Move trailers between the yard, the dock doors and nearby off-property lots.", + "do": [ + "Yard Drivers at {banner} #{store} in {city}, {state} shuttle trailers between dock doors, the " + "on-site yard and nearby off-property parking so that inbound and outbound freight never waits " + "on a door.", + "You will spend the shift in a yard tractor, outdoors in all weather, coordinating over radio " + "with the dock office and the guard shack.", + ], + "bring": [ + "Hold a valid Class A commercial driver's license with a clean motor vehicle record.", + "Spot and pull trailers safely in tight yard conditions, day or night.", + "Complete yard checks and record trailer locations in the yard management system.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("6088", "Part time", "SE", 22.25, 25.75, 2, None), + ("7133", "Part time", "WD", 23.10, 26.60, 1, None), + ("6014", "Full time", "WN", 22.80, 26.30, 3, None), + ("9399", "Part time", "FX,SN", 21.90, 25.40, 2, None), + ], + }, + { + "title": "Class A CDL Truck Driver", + "area": "supply-chain-and-transportation", + "category": "Drivers", + "hashtag": "#drivewithwalmart", + "summary": "Run scheduled store deliveries out of a private fleet transportation office.", + "do": [ + "Drivers based at {banner} #{store} in {city}, {state} run scheduled routes to stores and clubs " + "in the surrounding region, unloading with the store team and returning with backhaul freight.", + "Our private fleet runs newer equipment, publishes routes in advance and gets most drivers home " + "regularly. Safety scorecards are reviewed with your transportation manager every month.", + ], + "bring": [ + "Hold a valid Class A CDL and meet all federal Department of Transportation requirements.", + "At least 30 months of experience in the last 4 years driving a tractor trailer.", + "No preventable accidents or serious traffic violations in the last three years.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("6038", "Full time", "WD", 32.00, 41.00, 6, None), + ("6014", "Full time", "SD,WN", 31.50, 40.50, 4, None), + ("7133", "Full time", "WE", 30.75, 39.75, 5, None), + ("6088", "Full time", "WN", 33.25, 42.25, 3, None), + ], + }, + { + "title": "Asset Protection Associate - All DC/FC", + "area": "supply-chain-and-transportation", + "category": "Security and Asset Protection", + "hashtag": "#dcassetprotectionjobs", + "summary": "Protect people, product and property inside a distribution or fulfillment building.", + "do": [ + "Asset Protection Associates at {banner} #{store} in {city}, {state} control access at the guard " + "shack and associate entrances, audit trailer seals, and run the camera system that covers the " + "dock and the yard.", + "You will partner with operations leadership on safety walks, investigate shrink incidents, and " + "write up findings for the asset protection manager.", + ], + "bring": [ + "Control access to the building and the yard, verifying credentials at every entry point.", + "Audit inbound and outbound trailer seals against the manifest.", + "Monitor camera systems and document incidents accurately and promptly.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("9046", "Part time", "WD,FX", 22.00, 25.50, 2, None), + ("9054", "Part time", "SD,FX", 21.00, 24.50, 1, None), + ("6088", "Full time", "WN", 22.50, 26.00, 2, None), + ("7133", "Full time", "SN,FX", 23.00, 26.50, 1, None), + ("6014", "Part time", "WE", 20.75, 24.25, 3, None), + ], + }, + { + "title": "Facility Maintenance Technician", + "area": "supply-chain-and-transportation", + "category": "Engineering", + "hashtag": "#facilitymaintenancejobs", + "summary": "Keep conveyors, dock equipment and building systems running across the shift.", + "do": [ + "Facility Maintenance Technicians at {banner} #{store} in {city}, {state} perform preventive " + "maintenance and emergency repairs on conveyor systems, sortation equipment, dock levellers and " + "building services.", + "You will read schematics, troubleshoot electrical and mechanical faults, and close out work " + "orders in the maintenance system before the end of your shift.", + ], + "bring": [ + "Two years of industrial maintenance experience or a completed technical program.", + "Troubleshoot 480V three-phase systems, motor controls and pneumatics safely.", + "Read and work from electrical, mechanical and pneumatic schematics.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("9046", "Full time", "WD,FX", 26.50, 34.00, 2, None), + ("9281", "Full time", "WN,FX", 25.75, 33.25, 1, None), + ("6038", "Full time", "SD", 27.00, 34.50, 2, None), + ("7133", "Full time", "WE,FX", 26.00, 33.50, 3, None), + ("6014", "Part time", "FX", 25.25, 32.75, 1, None), + ("7021", "Full time", "WD,FX", 26.25, 33.75, 1, None), + ], + }, + { + "title": "Automation Technician", + "area": "supply-chain-and-transportation", + "category": "Engineering", + "hashtag": "#automationtechjobs", + "summary": "Support the robotics and controls that run our automated storage and retrieval systems.", + "do": [ + "Automation Technicians at {banner} #{store} in {city}, {state} maintain the robotics cells, " + "programmable controllers and vision systems behind our automated storage and retrieval " + "operation.", + "You will run diagnostics from the controls HMI, replace failed drives and sensors, and escalate " + "recurring faults to the controls engineering team with the data to back it up.", + ], + "bring": [ + "Experience maintaining PLC-controlled equipment, servo drives and industrial networks.", + "Comfort working at height and in confined maintenance aisles under lockout/tagout.", + "Track fault history and parts usage in the maintenance management system.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("9054", "Full time", "WN", 28.00, 36.00, 2, None), + ("9399", "Full time", "SN", 27.50, 35.50, 1, None), + ("6088", "Full time", "WD", 29.00, 37.00, 2, None), + ], + }, + { + "title": "Aviation Line Service Technician", + "area": "supply-chain-and-transportation", + "category": "Aviation", + "hashtag": "#aviationjobs", + "summary": "Fuel, tow and service company aircraft on the ramp at a fleet operations base.", + "do": [ + "Line Service Technicians supporting {banner} #{store} in {city}, {state} marshal, fuel, tow and " + "de-ice company aircraft, and keep the ramp and hangar to airfield standard.", + "You will work directly with flight crews and the maintenance team, following company and FAA " + "ground handling procedures on every movement.", + ], + "bring": [ + "Ramp, fueling or ground handling experience at a fixed base operator or airline.", + "Valid driver's license and the ability to obtain an airport security badge.", + "Careful documentation of every fuel load and aircraft movement.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("6014", "Full time", "WD", 24.00, 31.00, 1, None), + ("7133", "Part time", "SD", 23.50, 30.50, 1, None), + ("6088", "Part time", "WE", 22.75, 29.75, 2, None), + ("6038", "Full time", "FX,SN", 24.50, 31.50, 1, None), + ], + }, + { + "title": "Inventory Control Clerk", + "area": "supply-chain-and-transportation", + "category": "SC&T Operations", + "hashtag": "#inventorycontroljobs", + "summary": "Own cycle counts, research and the paperwork that keeps building inventory accurate.", + "do": [ + "Inventory Control Clerks at {banner} #{store} in {city}, {state} run daily cycle counts, " + "research variances between the system and the slot, and correct records so the building ships " + "what the store ordered.", + "Provides clerical and administrative support through generating and maintaining forms, reports " + "and logs via computerized management software, and communicating with the operations team on " + "open research.", + ], + "bring": [ + "Clerical duties (filing, keying, faxing), entering and extracting data from multiple systems.", + "Use of computer applications required (email, spreadsheets, word processing, and Microsoft Office).", + "The ability to be accurate and focus on attention to details will be critical.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("9281", "Part time", "WD,FX", 19.00, 22.50, 2, None), + ("9399", "Part time", "WE,FX", 19.60, 23.10, 1, None), + ("9046", "Full time", "SE,FX", 20.25, 23.75, 2, None), + ], + }, + # ----------------------------- Stores and Clubs ------------------------ + { + "title": "Cashier & Front End Services", + "area": "stores-and-clubs", + "category": "Cashier and Front-End Services", + "hashtag": "#frontendservicesjobs", + "summary": "Greet members and customers at the front end, ring transactions and keep lines moving.", + "do": [ + "At {banner} #{store} in {city}, {state} you are the last person a customer sees, so you set the " + "tone for the whole trip. You ring up orders quickly and accurately, bag with care, and answer " + "questions about returns, pickup and our app.", + "You will rotate across registers, self checkout and the service desk depending on the hour, and " + "you will be on your feet for most of the shift.", + ], + "bring": [ + "Ring transactions accurately and handle cash, cards and digital tenders.", + "Support self checkout, resolving item and payment issues for customers.", + "Process returns and exchanges at the service desk to policy.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("2503", "Part time", "WD,SD", 15.00, 24.00, 5, None), + ("2610", "Full time", "WD,WE,SD", 15.00, 24.00, 3, None), + ("3512", "Part time", "SE,SN", 15.00, 23.00, 4, None), + ("8763", "Part time", "SE,SN", 16.00, 25.00, 2, None), + ("2110", "Full time", "WD,WE", 15.50, 26.00, 4, None), + ("5382", "Part time", "WE,SE", 17.00, 28.00, 3, None), + ("3593", "Part time", "WE,SN", 15.00, 23.50, 4, None), + ("2075", "Part time", "WE,SE", 15.50, 25.50, 2, None), + ("3826", "Part time", "WE,SE", 15.00, 24.50, 2, None), + ], + }, + { + "title": "Cosmetics Cashier", + "area": "stores-and-clubs", + "category": "Cashier and Front-End Services", + "hashtag": "#cosmeticscashierjobs", + "summary": "Run the beauty counter register and keep the cosmetics department shoppable.", + "do": [ + "The cosmetics counter at {banner} #{store} in {city}, {state} has its own register and its own " + "regulars. You ring transactions there, help customers find shades and brands, and keep the " + "planogram faced and stocked.", + "You will also handle the department's security cases and coordinate restock with the general " + "merchandise team.", + ], + "bring": [ + "Ring transactions at a departmental register and reconcile the till at shift end.", + "Keep the beauty planogram faced, stocked and free of expired product.", + "Open locked cases for customers and follow high-theft merchandise procedures.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("2503", "Part time", "WD,WE", 15.00, 24.00, 1, None), + ("3520", "Part time", "SD,SE", 15.00, 24.00, 2, None), + ("2163", "Part time", "WE,SE", 15.00, 25.00, 1, None), + ], + }, + { + "title": "Member Services Associate", + "area": "stores-and-clubs", + "category": "Cashier and Front-End Services", + "hashtag": "#memberservicesjobs", + "summary": "Sign up new members, renew memberships and solve problems at the member services desk.", + "do": [ + "At {banner} #{store} in {city}, {state} you own the member services desk: new sign-ups, " + "renewals, upgrades, returns and the occasional tough conversation.", + "You will explain plan tiers and instant savings honestly, resolve billing questions, and hand " + "off anything you cannot fix to the club lead with the full picture.", + ], + "bring": [ + "Enroll, renew and upgrade memberships accurately in the membership system.", + "Process returns and refunds within club policy.", + "Explain plan benefits clearly without overselling.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("8259", "Full time", "WD,SD", 18.00, 26.00, 2, None), + ("6318", "Part time", "WE,SE", 16.00, 24.00, 3, None), + ("8763", "Full time", "WD,WE", 16.00, 24.00, 1, None), + ("4744", "Part time", "WE,SE", 16.50, 24.50, 3, None), + ("8155", "Full time", "WD,SD", 17.00, 25.00, 2, None), + ], + }, + { + "title": "Food & Grocery Associate", + "area": "stores-and-clubs", + "category": "Food and Grocery", + "hashtag": "#foodandgroceryjobs", + "summary": "Stock, rotate and merchandise the grocery aisles, coolers and freezers.", + "do": [ + "Food & Grocery Associates at {banner} #{store} in {city}, {state} unload the grocery truck, " + "stock dry, chilled and frozen departments, rotate dated product, and zone the aisles so the " + "store is shoppable at open.", + "You will handle food safety checks on your section and pull anything past code before it " + "reaches a customer.", + ], + "bring": [ + "Stock and rotate product following first-in, first-out standards.", + "Complete temperature and date checks for chilled and frozen sections.", + "Operate a manual and electric pallet jack after certification.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("2110", "Full time", "WD,WE", 15.50, 26.00, 3, None), + ("2050", "Part time", "WE,SE", 17.50, 28.00, 2, None), + ("471", "Full time", "WD,SD", 15.00, 25.00, 4, None), + ("1236", "Part time", "WN,SN", 16.00, 26.00, 2, {"job_id": "CP-1236-10741"}), + ("5388", "Full time", "WN", 16.50, 27.00, 2, None), + ("5133", "Full time", "WE,SN", 16.00, 26.50, 2, None), + ], + }, + { + "title": "Freezer/Cooler Associate", + "area": "stores-and-clubs", + "category": "Food and Grocery", + "hashtag": "#freezercoolerjobs", + "summary": "Work the club's freezer and cooler boxes, stocking bulk frozen and chilled product.", + "do": [ + "At {banner} #{store} in {city}, {state} you spend most of the shift inside the freezer and " + "cooler boxes, breaking down pallets of bulk frozen and chilled product and building the club " + "floor displays.", + "Cold weather gear is provided. You will follow scheduled warm-up breaks and log box " + "temperatures every rotation.", + ], + "bring": [ + "Work extended periods in temperatures as low as minus ten degrees Fahrenheit.", + "Break down pallets and build club floor displays to the merchandising plan.", + "Record freezer and cooler temperatures on the posted schedule.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("6608", "Full time", "WD,WN", 19.00, 27.00, 1, None), + ("4750", "Part time", "SN,WN", 17.50, 25.50, 2, None), + ("8253", "Full time", "WN,SN", 17.00, 25.00, 1, None), + ("8155", "Part time", "WN,SN", 17.50, 25.50, 2, None), + ], + }, + { + "title": "General Merchandise Associate", + "area": "stores-and-clubs", + "category": "General Merchandise, Stocking, and Unloading", + "hashtag": "#generalmerchandisejobs", + "summary": "Unload, sort and stock general merchandise across the sales floor.", + "do": [ + "General Merchandise Associates at {banner} #{store} in {city}, {state} unload trailers, sort " + "freight to department, stock shelves and pull back the overhead so the floor stays full.", + "You will work modular resets, price changes and seasonal transitions alongside the department " + "team lead.", + ], + "bring": [ + "Unload and sort freight accurately to department.", + "Stock, zone and face assigned departments to company standard.", + "Execute modular resets and seasonal transitions on schedule.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("2050", "Part time", "WN,SN", 17.50, 28.00, 3, None), + ("3387", "Part time", "WD,SD", 15.00, 25.00, 2, None), + ("1179", "Full time", "WE,SE,WN", 15.00, 25.00, 4, None), + ("954", "Part time", "WD,WE", 15.00, 24.00, 2, None), + ("5133", "Part time", "SD,SE", 15.50, 25.50, 3, None), + ], + }, + { + "title": "Stocking Associate", + "area": "stores-and-clubs", + "category": "General Merchandise, Stocking, and Unloading", + "hashtag": "#stockingassociatejobs", + "summary": "Work the overnight stocking team, filling the store before the doors open.", + "do": [ + "Stocking Associates at {banner} #{store} in {city}, {state} work the truck overnight: unload, " + "sort to aisle, stock, break down cardboard and zone the floor so the store looks new at open.", + "The pace is set by the truck. You will move steadily for the whole shift with a small team and " + "a clear finish line.", + ], + "bring": [ + "Stock assigned aisles completely and accurately before the store opens.", + "Break down and bale cardboard, keeping aisles clear and safe.", + "Use a manual pallet jack and rolltainers safely in tight aisles.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("1230", "Full time", "WN", 15.50, 24.50, 3, None), + ("2163", "Part time", "SN,WN", 16.50, 26.50, 2, None), + ("2075", "Full time", "WD,SD", 16.00, 25.00, 3, None), + ("3593", "Part time", "WN", 15.00, 24.00, 2, None), + ], + }, + { + "title": "Merchandising and Stocking Associate", + "area": "stores-and-clubs", + "category": "General Merchandise, Stocking, and Unloading", + "hashtag": "#merchandisingjobs", + "summary": "Build club pallets and keep the sales floor merchandised to plan.", + "do": [ + "At {banner} #{store} in {city}, {state} you stock bulk club pallets, build feature displays at " + "the action alley, and keep signage and pricing accurate across your zone.", + "You will use an electric pallet jack and order picker after certification, and you will work " + "with the merchandising lead on weekly display changes.", + ], + "bring": [ + "Stock club pallets and build feature displays to the merchandising plan.", + "Verify signage and pricing against the weekly plan every shift.", + "Operate an electric pallet jack and order picker after certification.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("4750", "Part time", "SN,WN", 17.00, 19.50, 3, None), + ("6318", "Part time", "SN,SD", 21.00, 24.50, 2, None), + ("6636", "Full time", "SN,WN", 18.00, 19.75, 4, None), + ("8253", "Part time", "SN,WE", 17.50, 19.25, 2, None), + ("6216", "Part time", "WN,WD", 18.50, 19.90, 3, None), + ("8763", "Part time", "SN,SE", 16.50, 18.75, 1, None), + ], + }, + { + "title": "Online Order Filling Team Associate", + "area": "stores-and-clubs", + "category": "Digital Pickup and Delivery", + "hashtag": "#onlineorderfillingjobs", + "summary": "Shop, stage and hand off customer pickup and delivery orders.", + "do": [ + "Online Order Filling Team Associates at {banner} #{store} in {city}, {state} shop customer " + "orders from the sales floor with a cart and a handheld, choose the freshest substitutions when " + "an item is out, and stage completed orders in the pickup coolers.", + "You will also load orders into customer vehicles at the pickup canopy and hand off to delivery " + "drivers on schedule.", + ], + "bring": [ + "Pick customer orders accurately against the handheld pick list.", + "Choose quality substitutions and communicate them to the customer.", + "Stage orders at the correct temperature and load them at the pickup canopy.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("1399", "Part time", "WD,WE,SD", 14.50, 27.50, 3, None), + ("144", "Full time", "WD,SD", 15.50, 26.00, 2, None), + ("5991", "Part time", "WE,SE", 17.50, 28.00, 4, None), + ("5260", "Full time", "WD,WE,SE", 15.00, 28.00, 2, None), + ("5388", "Part time", "WE,SE", 16.00, 27.00, 2, None), + ("2075", "Full time", "WD,WE", 15.50, 26.50, 3, None), + ("3826", "Full time", "WD,SD", 15.00, 26.00, 2, None), + ("3593", "Full time", "WD,SE", 14.50, 25.00, 4, None), + ], + }, + { + "title": "Online Order Filling Team Supervisor", + "area": "stores-and-clubs", + "category": "Digital Pickup and Delivery", + "hashtag": "#digitalpickupleadjobs", + "summary": "Lead the pickup and delivery team through the day's order volume.", + "do": [ + "The Online Order Filling Team Supervisor at {banner} #{store} in {city}, {state} runs the " + "digital team for the shift: assigns pickers, watches the order clock, and steps in wherever " + "the queue is tightest.", + "You will coach on pick quality and substitution decisions, handle escalated customer issues at " + "the canopy, and report on-time performance to the store lead each day.", + ], + "bring": [ + "Assign and balance pick work across the team through peak windows.", + "Coach associates on pick accuracy, substitutions and customer handoff.", + "Resolve escalated pickup and delivery issues at the canopy.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("2073", "Full time", "WD,SD", 20.00, 33.00, 2, None), + ("1179", "Full time", "WD,WE", 19.50, 32.00, 1, None), + ("5439", "Part time", "WE,SE", 19.00, 31.50, 2, None), + ], + }, + { + "title": "Cafe Associate", + "area": "stores-and-clubs", + "category": "Cafe", + "hashtag": "#cafeassociatejobs", + "summary": "Run the club cafe: prep, grill, serve and keep the counter to food safety standard.", + "do": [ + "Cafe Associates at {banner} #{store} in {city}, {state} take orders, prep and cook to the " + "posted recipe cards, and keep the counter, the drink station and the seating area clean " + "through the rush.", + "You will run opening or closing food safety checklists and log temperatures on every batch.", + ], + "bring": [ + "Prepare food to recipe and hold it at safe temperatures.", + "Complete opening, mid-shift and closing food safety logs.", + "Keep the counter, equipment and seating area clean and stocked.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("6608", "Part time", "WD,WE", 17.00, 24.00, 2, None), + ("8259", "Part time", "WD,SD", 16.00, 23.00, 1, None), + ("6318", "Part time", "SD,SE,FX", 15.50, 22.50, 3, None), + ("6636", "Part time", "WD,SD", 16.00, 23.00, 2, None), + ("4744", "Full time", "WD,SD", 16.50, 23.50, 2, None), + ], + }, + { + "title": "Team Lead", + "area": "stores-and-clubs", + "category": "Retail Management", + "hashtag": "#teamleadjobs", + "summary": "Lead a department team, own its standards and develop the associates on it.", + "do": [ + "Team Leads at {banner} #{store} in {city}, {state} run a department end to end: staffing the " + "shift, setting priorities at the huddle, working the floor alongside the team and owning the " + "department's sales and in-stock results.", + "You will coach associates day to day, handle customer escalations, and partner with the coach " + "on scheduling and development plans.", + ], + "bring": [ + "Plan and assign daily work for a department team.", + "Coach associates on standards and follow through on development plans.", + "Own department in-stock, shrink and customer experience results.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("144", "Full time", "WD,WE", 19.00, 32.00, 1, None), + ("2073", "Full time", "WD,WE", 20.00, 33.00, 1, None), + ("4137", "Full time", "WD,SD", 21.00, 34.00, 2, None), + ("2610", "Full time", "WE,SE", 18.00, 30.00, 1, None), + ], + }, + { + "title": "Coach", + "area": "stores-and-clubs", + "category": "Retail Management", + "hashtag": "#storeleadershipjobs", + "summary": "Lead several departments and the team leads who run them.", + "do": [ + "Coaches at {banner} #{store} in {city}, {state} lead a group of departments and the team leads " + "inside them, owning results for sales, availability, shrink and associate engagement across " + "that area of the building.", + "You will spend the day on the floor, remove barriers for your leads, and hold the standard " + "when the store is busiest.", + ], + "bring": [ + "Lead team leads and associates across multiple departments.", + "Own area results for sales, in-stock, shrink and engagement.", + "Build talent through structured coaching and succession planning.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("1230", "Full time", "WD,SD", 23.00, 38.00, 1, None), + ("2163", "Full time", "WE,SE", 24.00, 39.00, 1, None), + ], + }, + { + "title": "Fuel Station Associate", + "area": "stores-and-clubs", + "category": "Fuel Station", + "hashtag": "#fuelstationjobs", + "summary": "Run the club fuel station: assist members, check equipment and keep the site compliant.", + "do": [ + "Fuel Station Associates at {banner} #{store} in {city}, {state} greet members at the pumps, " + "help with payment issues, complete daily equipment and environmental checks, and keep the " + "island clean and stocked.", + "You will work outdoors in all weather and follow strict fuel handling and spill response " + "procedures.", + ], + "bring": [ + "Complete daily fuel equipment, tank and environmental compliance checks.", + "Assist members at the pump and resolve payment issues.", + "Follow fuel handling, spill response and emergency shutdown procedures.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("6636", "Part time", "WD,SD,FX", 16.50, 23.50, 2, None), + ("8253", "Part time", "WE,SE", 15.50, 22.50, 1, None), + ("6216", "Part time", "SN,WN", 17.00, 24.00, 2, None), + ("4750", "Full time", "WD,WN", 16.00, 23.00, 1, None), + ], + }, + { + "title": "Auto Care Center Technician", + "area": "stores-and-clubs", + "category": "Auto Care Center", + "hashtag": "#autocarecenterjobs", + "summary": "Perform tire, battery and light maintenance service in the Auto Care Center.", + "do": [ + "Auto Care Center Technicians at {banner} #{store} in {city}, {state} mount and balance tires, " + "install batteries, change oil and complete light maintenance services while the customer " + "shops.", + "You will inspect vehicles honestly, document every service performed, and keep the bay and " + "equipment to safety standard.", + ], + "bring": [ + "Mount, balance and repair tires and install batteries safely.", + "Complete oil changes and light maintenance to manufacturer specification.", + "Document every inspection and service accurately in the shop system.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("954", "Full time", "WD,SD", 17.00, 30.00, 3, None), + ("1230", "Full time", "WD,WE", 17.00, 30.00, 5, None), + ("471", "Part time", "WE,SE", 16.50, 29.00, 2, None), + ("1179", "Full time", "WD,SD", 17.50, 30.50, 1, None), + ("5133", "Full time", "WD,SD", 17.50, 30.50, 2, None), + ], + }, + { + "title": "Tire & Battery Technician", + "area": "stores-and-clubs", + "category": "Auto Services", + "hashtag": "#tireandbatteryjobs", + "summary": "Service member vehicles in the club tire and battery center.", + "do": [ + "Tire & Battery Technicians at {banner} #{store} in {city}, {state} install and rotate tires, " + "test and replace batteries, and complete the free member services the club is known for.", + "You will work the service write-up desk as well as the bay, so clear explanations matter as " + "much as clean work.", + ], + "bring": [ + "Install, rotate and repair tires to torque and safety specification.", + "Test and replace batteries and charging system components.", + "Write up member services clearly and set accurate expectations on timing.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("6608", "Full time", "WD,WE", 19.00, 27.00, 2, None), + ("4750", "Part time", "SD,SE", 17.50, 25.50, 1, None), + ("6216", "Full time", "WD,SD", 18.50, 26.50, 2, None), + ("8259", "Part time", "WE,SN", 17.00, 25.00, 1, None), + ], + }, + { + "title": "Maintenance Technician", + "area": "stores-and-clubs", + "category": "Maintenance", + "hashtag": "#storemaintenancejobs", + "summary": "Keep store equipment, refrigeration and building systems running.", + "do": [ + "Maintenance Technicians at {banner} #{store} in {city}, {state} respond to equipment calls " + "across the building: refrigeration alarms, doors, carts, lighting, HVAC and the compactor.", + "You will complete scheduled preventive maintenance, escalate refrigerant work to the " + "certified contractor, and close every work order with what you actually did.", + ], + "bring": [ + "Diagnose and repair store equipment, lighting and building systems.", + "Complete scheduled preventive maintenance on time.", + "Follow lockout/tagout and electrical safety procedures without exception.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("2073", "Full time", "WN,SN,FX", 20.00, 30.00, 1, None), + ("1399", "Part time", "WE,SE,FX", 18.00, 28.00, 2, None), + ("5439", "Full time", "WD,WE,FX", 19.00, 29.00, 1, None), + ("3512", "Full time", "WD,SD", 17.00, 26.00, 2, None), + ], + }, + { + "title": "Asset Protection Associate", + "area": "stores-and-clubs", + "category": "Security and Asset Protection", + "hashtag": "#assetprotectionjobs", + "summary": "Reduce shrink and keep associates and customers safe inside the store.", + "do": [ + "Asset Protection Associates at {banner} #{store} in {city}, {state} work the floor and the " + "camera room, deter theft, respond to alarms, and partner with store leadership on safety " + "walks and incident follow-up.", + "You will document every incident to policy and work with local law enforcement when the " + "asset protection manager asks you to.", + ], + "bring": [ + "Deter and document theft following company approach and apprehension policy.", + "Monitor camera and EAS systems and respond to alarms.", + "Complete safety walks and incident reports accurately.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("5991", "Full time", "WD,WE,SD", 17.00, 30.00, 2, None), + ("3387", "Part time", "SE,SN", 16.00, 28.00, 1, None), + ("5382", "Full time", "WD,SD", 19.00, 32.00, 1, None), + ("5388", "Full time", "WD,SD", 18.00, 30.00, 2, None), + ], + }, + { + "title": "Asset Protection Customer Specialist", + "area": "stores-and-clubs", + "category": "Security and Asset Protection", + "hashtag": "#apcustomerspecialistjobs", + "summary": "Greet at the entrance, verify receipts and keep the front of the store secure.", + "do": [ + "Asset Protection Customer Specialists at {banner} #{store} in {city}, {state} work the " + "entrance: greeting every customer, verifying receipts at the door, and watching the front end " + "for problems before they grow.", + "You will support the asset protection team with documentation and keep the entry area clean, " + "carted and welcoming.", + ], + "bring": [ + "Greet customers at the entrance and verify receipts at the exit.", + "Watch front-end activity and escalate concerns to asset protection.", + "Keep the entry area stocked with carts and free of hazards.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("2503", "Full time", "WD,WE", 16.00, 26.00, 1, None), + ("5260", "Part time", "WE,SE", 17.00, 28.00, 2, None), + ], + }, + # ------------------------------- Healthcare ---------------------------- + { + "title": "Pharmacy Technician", + "area": "healthcare", + "category": "Pharmacy Services", + "hashtag": "#pharmacytechjobs", + "summary": "Support the pharmacist with intake, data entry, filling and patient pickup.", + "do": [ + "Pharmacy Technicians at {banner} #{store} in {city}, {state} take in prescriptions, enter and " + "verify patient and insurance information, count and label under the pharmacist's supervision, " + "and hand off at the pickup window.", + "You will work third-party rejections, call prescribers for clarifications, and keep the " + "workflow moving so patients are not waiting on paperwork.", + ], + "bring": [ + "Enter prescription and insurance information accurately into the pharmacy system.", + "Fill and label prescriptions under the direct supervision of the pharmacist.", + "Resolve third-party rejections and coordinate with prescriber offices.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("5260", "Full time", "WD,SD", 18.00, 30.00, 2, None), + ("4137", "Part time", "WE,SE", 19.50, 32.00, 1, None), + ("2110", "Full time", "WD,WE", 18.50, 30.50, 1, None), + ("2050", "Part time", "SD,SE", 20.00, 33.00, 2, None), + ("3826", "Full time", "WD,WE", 18.50, 31.00, 3, None), + ], + }, + { + "title": "Certified Pharmacy Technician", + "area": "healthcare", + "category": "Pharmacy Services", + "hashtag": "#certifiedpharmacytechjobs", + "summary": "Work at the top of your certification supporting immunizations and clinical services.", + "do": [ + "Certified Pharmacy Technicians at {banner} #{store} in {city}, {state} do everything a " + "technician does, plus the work that certification unlocks: immunization support, medication " + "therapy outreach and inventory ownership for controlled substances.", + "You will mentor uncertified technicians on workflow and accuracy, and cover the pharmacist's " + "administrative queue during clinical blocks.", + ], + "bring": [ + "Hold and maintain a current state pharmacy technician certification.", + "Support immunization clinics and medication therapy outreach.", + "Own perpetual inventory counts for controlled substances.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("3520", "Full time", "WD,WE", 21.00, 34.00, 1, None), + ("1236", "Part time", "WE,SD", 19.00, 31.00, 2, None), + ], + }, + { + "title": "Optician", + "area": "healthcare", + "category": "Optical Services", + "hashtag": "#opticianjobs", + "summary": "Fit, adjust and dispense eyewear in the Vision Center.", + "do": [ + "Opticians at {banner} #{store} in {city}, {state} interpret prescriptions, take measurements, " + "recommend lens options honestly, and fit and adjust finished eyewear so it is comfortable on " + "day one.", + "You will also run the lab bench work the store handles in house, manage the frame board, and " + "coordinate with the independent optometrist's office next door.", + ], + "bring": [ + "Interpret ophthalmic prescriptions and take accurate fitting measurements.", + "Recommend frames and lens treatments suited to the prescription and budget.", + "Adjust, repair and dispense finished eyewear to specification.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("2073", "Full time", "WD,SD", 21.50, 34.50, 1, None), + ("5382", "Part time", "WE,SE", 23.00, 36.00, 1, None), + ("1236", "Full time", "WD,WE", 21.00, 34.00, 1, None), + ("5991", "Full time", "WD,WE,SD", 22.00, 35.00, 2, None), + ("5133", "Full time", "WD,SD", 21.50, 34.50, 1, None), + ("3826", "Part time", "WE,SE", 22.50, 35.50, 1, None), + ], + }, + { + "title": "Vision Center Associate", + "area": "healthcare", + "category": "Optical Services", + "hashtag": "#visioncenterjobs", + "summary": "Greet vision center customers, schedule exams and support the optician.", + "do": [ + "Vision Center Associates at {banner} #{store} in {city}, {state} welcome customers, schedule " + "exams with the on-site optometrist, verify vision benefits and support the optician with " + "dispensing and repairs.", + "You will keep the frame board merchandised and the exam schedule full without overbooking.", + ], + "bring": [ + "Schedule exams and verify vision insurance benefits.", + "Support dispensing, adjustments and simple frame repairs.", + "Keep the frame board merchandised, priced and clean.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("471", "Part time", "WD,SD", 16.00, 26.00, 2, None), + ("5439", "Part time", "WE,SE", 17.00, 27.00, 1, None), + ], + }, + { + "title": "Health & Wellness Operations Associate", + "area": "healthcare", + "category": "Health and Wellness Operations", + "hashtag": "#healthandwellnessjobs", + "summary": "Keep the health and wellness area stocked, compliant and ready for patients.", + "do": [ + "Health & Wellness Operations Associates at {banner} #{store} in {city}, {state} own the " + "operational side of the department: over-the-counter stocking, expiration audits, compliance " + "logs and the patient waiting area.", + "You will support screening events, keep the private consultation room ready, and route " + "patient questions to the pharmacist correctly.", + ], + "bring": [ + "Complete over-the-counter stocking and expiration audits on schedule.", + "Maintain compliance logs and the private consultation area.", + "Support screening and immunization events with setup and intake.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("2050", "Full time", "WD,WE", 18.00, 29.00, 1, None), + ("3387", "Part time", "SD,SE", 17.00, 28.00, 2, None), + ("3512", "Part time", "WD,SD", 17.50, 28.50, 1, None), + ("1399", "Part time", "WE,SN", 18.50, 29.50, 2, None), + ], + }, + { + "title": "Certified Medical Assistant", + "area": "healthcare", + "category": "Clinical Care", + "hashtag": "#clinicalcarejobs", + "summary": "Room patients, take vitals and support the clinician in a community care setting.", + "do": [ + "Certified Medical Assistants supporting {banner} #{store} in {city}, {state} greet and room " + "patients, take vitals and history, prepare the room, and assist the clinician during the " + "visit.", + "You will document in the electronic health record, handle specimen collection and labelling, " + "and close out visit instructions with the patient before they leave.", + ], + "bring": [ + "Hold a current medical assistant certification and BLS card.", + "Take and document vitals, history and medication reconciliation accurately.", + "Collect and label specimens following chain-of-custody procedures.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("1236", "Full time", "WD,WE", 19.00, 30.00, 1, None), + ("3520", "Part time", "WD,SD", 22.00, 34.00, 2, None), + ("2610", "Part time", "WE,SE", 19.50, 30.50, 1, None), + ("954", "Part time", "WD,SD", 18.00, 29.00, 2, None), + ], + }, + # ------------------------------- Students (hourly) --------------------- + { + "title": "Retail Operations Intern", + "area": "students", + "category": "Internship", + "hashtag": "#walmartinternships", + "summary": "A paid store internship rotating through front end, digital and merchandising.", + "do": [ + "Retail Operations Interns at {banner} #{store} in {city}, {state} spend the term rotating " + "through the front end, the digital pickup team and a merchandising department, with a store " + "leader as your mentor.", + "You will finish the program by presenting one operational improvement you scoped, tested and " + "measured inside the building.", + ], + "bring": [ + "Currently enrolled in an associate or bachelor's degree program.", + "Availability for a full internship term including some weekend coverage.", + "Willingness to work the floor in every rotation, not just observe.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("144", "Intern", "WD,FX", 16.00, 20.00, 1, None), + ("2073", "Intern", "WD,FX", 16.50, 20.50, 1, None), + ("4137", "Intern", "WE,FX", 17.00, 21.00, 1, None), + ("2503", "Intern", "WD,SD", 15.00, 19.00, 1, None), + ("2050", "Intern", "WD,FX", 16.25, 20.25, 1, None), + ("1179", "Intern", "WE,FX", 16.75, 20.75, 1, None), + ("3387", "Intern", "WD,SD", 15.50, 19.50, 1, None), + ], + }, + { + "title": "Club Operations Intern", + "area": "students", + "category": "Internship", + "hashtag": "#samsclubinternships", + "summary": "A paid club internship focused on membership growth and fresh operations.", + "do": [ + "Club Operations Interns at {banner} #{store} in {city}, {state} work with the club manager on " + "membership growth, fresh area operations and the weekly merchandising plan.", + "You will own one measurable project for the term and present the results to the club " + "leadership team.", + ], + "bring": [ + "Currently enrolled in an associate or bachelor's degree program.", + "Interest in retail operations, membership models or fresh category management.", + "Availability for a full internship term including some weekend coverage.", + "Complies with company policies, procedures, and standards of ethics and integrity.", + ], + "placements": [ + ("6608", "Intern", "WD,FX", 18.00, 22.00, 1, None), + ("6318", "Intern", "WE,FX", 16.50, 20.50, 1, None), + ("8259", "Intern", "WD,SD", 17.25, 21.25, 1, None), + ("6216", "Intern", "WE,FX", 17.75, 21.75, 1, None), + ("4744", "Intern", "WD,FX", 17.00, 21.00, 1, None), + ("8155", "Intern", "WE,FX", 17.50, 21.50, 1, None), + ], + }, +] + +# --------------------------------------------------------------------------- # +# Salaried title families. +# +# placement tuple: (store_number, employment_type, min_pay, max_pay, +# worker_type, qual_slots, extras) +# `qual_slots` = (degree_field, option1_years, option2_years, preferred_slot) +# `extras` is an optional dict: {"job_id": ..., "qual_clause": ...} +# Minimum-qualification text is built from the family's template with those +# slots, so every posting's Option 1 / Option 2 text is unique. A `qual_clause` +# is appended to both options ("..., including .") so that posting's +# qualification text is specific to this mirror. +# +# Pinned job_ids are synthetic: none of them is a requisition ID that exists on +# careers.walmart.com. +# --------------------------------------------------------------------------- # +SALARIED_FAMILIES = [ + { + "title": "Staff, Software Engineer - Backend / ML", + "area": "technology", + "category": "Software Engineering and Architecture", + "shifts": "WD,SD", + "summary": "Set the technical direction for backend microservices and ML-serving " + "infrastructure at retail scale.", + "do": [ + "As a Staff Software Engineer at {location_name} in {city}, {state}, you'll be a technical " + "leader who defines the direction for and evolves the backend microservices, data pipelines, " + "and ML-serving infrastructure that power search at massive scale. You'll lead a team of six " + "to ten engineers, set the technical vision for critical systems, and drive the quality bar " + "across the team.", + "We're in an active phase of platform modernization - redesigning and refactoring core " + "systems. If you want to build, not just maintain, this is the right time to join.", + ], + "about_team": "The eCommerce Search engineering team owns the end-to-end technology stack that " + "powers product search and discovery across Walmart's global eCommerce channels, " + "backed by microservices, large-scale data and feature pipelines, search engines, " + "and ML model serving infrastructure.", + "min_qual_option1": "Option 1: Bachelor's degree in {degree_field} and {y1} years' experience in " + "software engineering or related area.", + "min_qual_option2": "Option 2: {y2} years' experience in software engineering or related area.", + "preferred": "Master's degree in {degree_field} and {yp} years' experience in software " + "engineering or related area. We value candidates with a background in creating " + "inclusive digital experiences and knowledge of Web Content Accessibility " + "Guidelines (WCAG) 2.2 AA standards.", + "placements": [ + ("11807", "Full time", 143000, 286000, "Regular/Permanent", + ("computer science, computer engineering, computer information systems, software engineering, or related area", 5, 7, 2), + {"job_id": "R-2468347", + "qual_clause": "including experience operating search or ML-serving systems in production"}), + ("10101", "Full time", 132000, 264000, "Regular/Permanent", + ("computer science, computer engineering, or related area", 5, 8, 3), None), + ("12200", "Full time", 128000, 246000, "Regular/Permanent", + ("computer science, electrical engineering, or related area", 6, 9, 3), None), + ], + }, + { + "title": "Senior Software Engineer", + "area": "technology", + "category": "Software Engineering and Architecture", + "shifts": "WD", + "summary": "Design, build and operate the services behind checkout, fulfillment and search.", + "do": [ + "Senior Software Engineers at {location_name} in {city}, {state} own services end to end: " + "design, implementation, deployment and on-call. You will partner with product and data " + "science to turn ambiguous problems into systems that hold up at Walmart's traffic.", + "You will review designs and code across the team, mentor engineers earlier in their careers, " + "and keep an eye on cost, latency and reliability as much as on features.", + ], + "about_team": "This team builds and runs the platform services that thousands of engineers and " + "millions of customers depend on every day.", + "min_qual_option1": "Option 1: Bachelor's degree in {degree_field} and {y1} years' experience in " + "software engineering or related area.", + "min_qual_option2": "Option 2: {y2} years' experience in software engineering or related area.", + "preferred": "Master's degree in {degree_field} and {yp} years' experience building distributed " + "systems in production.", + "placements": [ + ("11003", "Full time", 110000, 220000, "Regular/Permanent", + ("computer science, computer information systems, or related area", 4, 7, 1), + {"qual_clause": "including experience building high-volume checkout or payments services"}), + ("11807", "Full time", 117000, 234000, "Regular/Permanent", + ("computer engineering, software engineering, or related area", 4, 7, 2), None), + ("10101", "Full time", 96000, 192000, "Regular/Permanent", + ("information systems, computer science, or related area", 2, 4, 1), None), + ("12200", "Full time", 105000, 195000, "Regular/Permanent", + ("embedded systems, computer engineering, or related area", 3, 6, 2), None), + ], + }, + { + "title": "Software Engineer III", + "area": "technology", + "category": "Software Engineering and Architecture", + "shifts": "WD", + "summary": "Build features across the smart TV platform and its content services.", + "do": [ + "Software Engineers at {location_name} in {city}, {state} build and ship features across the " + "platform, from the on-device experience to the services behind it.", + "You will write production code every week, take part in design reviews, and work with QA and " + "product on release readiness.", + ], + "about_team": "The platform engineering group builds the software that runs on millions of " + "connected devices in customers' living rooms.", + "min_qual_option1": "Option 1: Bachelor's degree in {degree_field} and {y1} years' experience in " + "software engineering or related area.", + "min_qual_option2": "Option 2: {y2} years' experience in software engineering or related area.", + "preferred": "Experience with embedded platforms and {yp} years' experience shipping consumer " + "software at scale.", + "placements": [ + ("12200", "Full time", 90000, 180000, "Regular/Permanent", + ("computer science or related area", 2, 4, 3), {"job_id": "R-2417063"}), + ], + }, + { + "title": "Senior Manager, Product Management", + "area": "technology", + "category": "Product Management", + "shifts": "WD", + "summary": "Own a product area end to end, from strategy through launch and iteration.", + "do": [ + "Senior Managers of Product Management at {location_name} in {city}, {state} own a product " + "area: the strategy, the roadmap, the trade-offs and the results.", + "You will work daily with engineering, design and data science, and you will be the person " + "who says no often enough that the yes means something.", + ], + "about_team": "Product management at Walmart sits close to the customer and close to the code.", + "min_qual_option1": "Option 1: Bachelor's degree in {degree_field} and {y1} years' experience in " + "product management or related area.", + "min_qual_option2": "Option 2: {y2} years' experience in product management or related area.", + "preferred": "Master's degree in business administration and {yp} years' experience leading " + "product teams.", + "placements": [ + ("10101", "Full time", 110000, 220000, "Regular/Permanent", + ("business, analytics, engineering, or related area", 5, 7, 2), {"job_id": "R-2418512"}), + ("11807", "Full time", 132000, 264000, "Regular/Permanent", + ("computer science, business, or related area", 6, 9, 3), None), + ("11003", "Full time", 90000, 180000, "Regular/Permanent", + ("marketing, business, or related area", 4, 6, 1), None), + ("12200", "Full time", 118000, 225000, "Regular/Permanent", + ("electrical engineering, product design, or related area", 5, 8, 3), None), + ], + }, + { + "title": "Director, Product Management", + "area": "technology", + "category": "Product Management", + "shifts": "WD,SD", + "summary": "Lead a portfolio of product areas and the managers who run them.", + "do": [ + "Directors of Product Management at {location_name} in {city}, {state} set direction for a " + "portfolio, hire and develop product managers, and represent the portfolio in company-level " + "planning.", + "You will spend your time on strategy, talent and unblocking - not on writing every " + "requirement yourself.", + ], + "about_team": "This portfolio spans several teams working on connected customer experiences.", + "min_qual_option1": "Option 1: Bachelor's degree in {degree_field} and {y1} years' experience in " + "product management or related area, including {yp} years of people leadership.", + "min_qual_option2": "Option 2: {y2} years' experience in product management or related area, " + "including {yp} years of people leadership.", + "preferred": "Master's degree in business administration and experience owning a profit and loss " + "statement for {yp} years or more.", + "placements": [ + ("10101", "Full time", 130000, 260000, "Regular/Permanent", + ("business, engineering, or related area", 8, 11, 4), None), + ("12200", "Full time", 125000, 245000, "Regular/Permanent", + ("electrical engineering, business, or related area", 7, 10, 4), None), + ], + }, + { + "title": "Senior Data Scientist", + "area": "technology", + "category": "Data Science and Analytics", + "shifts": "WD", + "summary": "Build models that change what customers see and what the business decides.", + "do": [ + "Senior Data Scientists at {location_name} in {city}, {state} frame the problem, build the " + "model, ship it behind an experiment, and tell the story of what it did.", + "You will work in Python and SQL against very large datasets, and you will be expected to " + "defend your methodology to people who will use the results.", + ], + "about_team": "Data science sits inside the product teams here, not in a separate lab.", + "min_qual_option1": "Option 1: Bachelor's degree in {degree_field} and {y1} years' experience in " + "an analytics or data science role.", + "min_qual_option2": "Option 2: {y2} years' experience in an analytics or data science role.", + "preferred": "Master's or PhD in {degree_field} and {yp} years' experience deploying models to " + "production.", + "placements": [ + ("10101", "Full time", 108000, 216000, "Regular/Permanent", + ("statistics, economics, computer science, or related area", 4, 6, 2), None), + ("11807", "Full time", 130000, 260000, "Regular/Permanent", + ("machine learning, statistics, or related area", 5, 8, 3), None), + ("11003", "Full time", 110000, 190000, "Regular/Permanent", + ("applied mathematics, statistics, or related area", 3, 5, 1), None), + ("11500", "Full time", 100000, 175000, "Regular/Permanent", + ("operations research, statistics, or related area", 3, 6, 2), None), + ("12200", "Full time", 112000, 205000, "Regular/Permanent", + ("data science, statistics, or related area", 4, 7, 2), None), + ], + }, + { + "title": "Senior Manager, Information Security", + "area": "technology", + "category": "Information Security", + "shifts": "WD,SD", + "summary": "Lead a security function protecting customer and associate data at scale.", + "do": [ + "Senior Managers of Information Security at {location_name} in {city}, {state} lead a security " + "team, set the control standard for their domain, and partner with engineering on how those " + "controls actually get implemented.", + "You will own incident response readiness for your area and report risk posture to leadership " + "on a regular cadence.", + ], + "about_team": "Information security here is embedded with the teams it protects.", + "min_qual_option1": "Option 1: Bachelor's degree in {degree_field} and {y1} years' experience in " + "information security or related area.", + "min_qual_option2": "Option 2: {y2} years' experience in information security or related area.", + "preferred": "CISSP or equivalent certification and {yp} years' experience leading security teams.", + "placements": [ + ("10101", "Full time", 115000, 230000, "Regular/Permanent", + ("information technology, cybersecurity, or related area", 5, 8, 3), None), + ("11807", "Full time", 140000, 280000, "Regular/Permanent", + ("computer science, cybersecurity, or related area", 6, 9, 4), None), + ("12200", "Full time", 122000, 235000, "Regular/Permanent", + ("information assurance, computer science, or related area", 4, 7, 3), None), + ], + }, + { + "title": "Information Security Engineer III", + "area": "technology", + "category": "Information Security", + "shifts": "WD", + "summary": "Engineer and operate the detection and prevention controls that protect the platform.", + "do": [ + "Security Engineers at {location_name} in {city}, {state} build detections, tune controls, and " + "work incidents alongside the response team.", + "You will write code, not just configure tools, and you will be on a rotation.", + ], + "about_team": "This team keeps the platform defensible as it changes weekly.", + "min_qual_option1": "Option 1: Bachelor's degree in {degree_field} and {y1} years' experience in " + "information security or related area.", + "min_qual_option2": "Option 2: {y2} years' experience in information security or related area.", + "preferred": "Experience with cloud security tooling and {yp} years' experience in detection " + "engineering.", + "placements": [ + ("11003", "Full time", 80000, 160000, "Regular/Permanent", + ("cybersecurity, information systems, or related area", 2, 4, 1), None), + ("12200", "Full time", 95000, 170000, "Regular/Permanent", + ("computer engineering, cybersecurity, or related area", 3, 5, 2), None), + ], + }, + { + "title": "Senior UX Designer", + "area": "technology", + "category": "Creative Design and UX", + "shifts": "WD", + "summary": "Design flows that millions of people use without thinking about them.", + "do": [ + "Senior UX Designers at {location_name} in {city}, {state} own the experience for a product " + "area: research synthesis, flows, prototypes and the detailed specs engineering builds from.", + "You will test your work with real customers and change it when the test says so.", + ], + "about_team": "Design partners directly with product and engineering from the first week of a " + "project.", + "min_qual_option1": "Option 1: Bachelor's degree in {degree_field} and {y1} years' experience in " + "user experience design or related area.", + "min_qual_option2": "Option 2: {y2} years' experience in user experience design or related area.", + "preferred": "A portfolio showing shipped consumer work and {yp} years' experience with design " + "systems.", + "placements": [ + ("11807", "Full time", 120000, 240000, "Regular/Permanent", + ("design, human-computer interaction, or related area", 5, 7, 3), None), + ("11003", "Full time", 96000, 186000, "Regular/Permanent", + ("interaction design, visual design, or related area", 3, 6, 2), None), + ("12200", "Full time", 88000, 165000, "Regular/Permanent", + ("industrial design, human factors, or related area", 3, 5, 2), None), + ], + }, + { + "title": "Principal UX Researcher", + "area": "technology", + "category": "Creative Design and UX", + "shifts": "WD", + "summary": "Set the research agenda for a large product organization.", + "do": [ + "Principal UX Researchers at {location_name} in {city}, {state} decide what the organization " + "needs to learn next, design the studies that answer it, and make sure the answer changes " + "what gets built.", + "You will mentor researchers across teams and raise the methodological bar for everyone.", + ], + "about_team": "Research here reports into design and works across several product areas at once.", + "min_qual_option1": "Option 1: Master's degree in {degree_field} and {y1} years' experience in " + "user research or related area.", + "min_qual_option2": "Option 2: {y2} years' experience in user research or related area.", + "preferred": "PhD in {degree_field} and {yp} years' experience leading mixed-methods research " + "programs.", + "placements": [ + ("11807", "Full time", 150000, 275000, "Regular/Permanent", + ("psychology, human-computer interaction, or related area", 7, 10, 4), None), + ("12200", "Full time", 138000, 255000, "Regular/Permanent", + ("cognitive science, design research, or related area", 6, 9, 3), None), + ], + }, + { + "title": "Senior Technical Program Manager", + "area": "technology", + "category": "Technical Program Management", + "shifts": "WD", + "summary": "Drive cross-team technical programs from commitment to launch.", + "do": [ + "Senior Technical Program Managers at {location_name} in {city}, {state} own the plan, the " + "risks and the communication for programs that span several engineering teams.", + "You will be technical enough to challenge an estimate and organized enough that nobody has " + "to ask you for a status.", + ], + "about_team": "Technical program management sits with engineering leadership here.", + "min_qual_option1": "Option 1: Bachelor's degree in {degree_field} and {y1} years' experience in " + "technical program management or related area.", + "min_qual_option2": "Option 2: {y2} years' experience in technical program management or related " + "area.", + "preferred": "Experience running programs across distributed teams for {yp} years or more.", + "placements": [ + ("11807", "Full time", 125000, 250000, "Regular/Permanent", + ("engineering, computer science, or related area", 5, 8, 3), None), + ("10101", "Full time", 105000, 210000, "Regular/Permanent", + ("information systems, engineering, or related area", 4, 7, 2), None), + ("12200", "Full time", 92000, 175000, "Regular/Permanent", + ("electrical engineering, computer science, or related area", 3, 6, 2), None), + ("11003", "Full time", 99000, 195000, "Regular/Permanent", + ("industrial engineering, business, or related area", 4, 6, 2), None), + ], + }, + { + "title": "IT Support Engineer", + "area": "technology", + "category": "Information Technology", + "shifts": "WD,SD", + "summary": "Keep the people who work here productive, from laptops to conference rooms.", + "do": [ + "IT Support Engineers at {location_name} in {city}, {state} handle escalated endpoint, " + "identity and collaboration issues for the associates on site.", + "You will automate the repeat offenders instead of fixing them one ticket at a time.", + ], + "about_team": "Workplace technology supports every associate in the building.", + "min_qual_option1": "Option 1: Associate's degree in {degree_field} and {y1} years' experience in " + "information technology support or related area.", + "min_qual_option2": "Option 2: {y2} years' experience in information technology support or " + "related area.", + "preferred": "Scripting experience and {yp} years' experience with endpoint management tooling.", + "placements": [ + ("10101", "Full time", 70000, 140000, "Regular/Permanent", + ("information technology or related area", 2, 4, 2), None), + ("11500", "Full time", 68000, 136000, "Regular/Permanent", + ("computer information systems or related area", 2, 5, 1), None), + ("12200", "Full time", 72000, 144000, "Regular/Permanent", + ("network administration or related area", 3, 5, 2), None), + ("11109", "Full time", 66000, 132000, "Regular/Permanent", + ("information systems or related area", 1, 3, 1), None), + ], + }, + { + "title": "Senior Manager, Finance", + "area": "corporate", + "category": "Accounting and Finance", + "shifts": "WD", + "summary": "Lead financial planning and analysis for a business unit.", + "do": [ + "Senior Managers of Finance at {location_name} in {city}, {state} run the planning cycle for " + "their business unit, build the models leadership decides from, and lead a small team of " + "analysts.", + "You will be in the room when the trade-offs are made, and you will be expected to have a " + "point of view.", + ], + "about_team": "Finance partners are embedded with the businesses they support.", + "min_qual_option1": "Option 1: Bachelor's degree in {degree_field} and {y1} years' experience in " + "accounting, finance or related area.", + "min_qual_option2": "Option 2: {y2} years' experience in accounting, finance or related area.", + "preferred": "CPA or MBA and {yp} years' experience leading finance teams.", + "placements": [ + ("10101", "Full time", 100000, 200000, "Regular/Permanent", + ("accounting, finance, or related area", 5, 7, 3), None), + ("11500", "Full time", 90000, 180000, "Regular/Permanent", + ("finance, economics, or related area", 4, 6, 2), None), + ("12200", "Full time", 98000, 190000, "Regular/Permanent", + ("corporate finance, accounting, or related area", 3, 5, 2), None), + ], + }, + { + "title": "Financial Analyst III", + "area": "corporate", + "category": "Accounting and Finance", + "shifts": "WD", + "summary": "Build the forecasts, variance analysis and business cases the team runs on.", + "do": [ + "Financial Analysts at {location_name} in {city}, {state} own a piece of the forecast, explain " + "variances to plan, and build the business cases that support investment decisions.", + "You will live in spreadsheets and the planning system, and you will present your work " + "directly to business leaders.", + ], + "about_team": "This team supports one of the largest cost centers in the company.", + "min_qual_option1": "Option 1: Bachelor's degree in {degree_field} and {y1} years' experience in " + "financial analysis or related area.", + "min_qual_option2": "Option 2: {y2} years' experience in financial analysis or related area.", + "preferred": "Advanced modelling skills and {yp} years' experience in a retail or supply chain " + "finance team.", + "placements": [ + ("10101", "Full time", 70000, 130000, "Regular/Permanent", + ("finance, accounting, or related area", 2, 4, 2), None), + ("11109", "Full time", 68000, 126000, "Regular/Permanent", + ("accounting, business, or related area", 2, 5, 1), None), + ("11500", "Full time", 72000, 134000, "Regular/Permanent", + ("economics, finance, or related area", 3, 5, 2), None), + ("12200", "Full time", 71000, 132000, "Regular/Permanent", + ("finance, business analytics, or related area", 1, 3, 1), None), + ], + }, + { + "title": "Senior Manager, People Partner", + "area": "corporate", + "category": "Human Resources", + "shifts": "WD", + "summary": "Partner with business leaders on talent, org design and associate experience.", + "do": [ + "Senior Managers, People Partner at {location_name} in {city}, {state} advise leaders on " + "organisation design, talent planning and the hard conversations, and own the people plan for " + "their client group.", + "You will use data as well as judgement, and you will be the person associates trust to be " + "straight with them.", + ], + "about_team": "People partners support the business teams in the building directly.", + "min_qual_option1": "Option 1: Bachelor's degree in {degree_field} and {y1} years' experience in " + "human resources or related area.", + "min_qual_option2": "Option 2: {y2} years' experience in human resources or related area.", + "preferred": "SHRM-SCP certification and {yp} years' experience supporting technology " + "organisations.", + "placements": [ + ("10101", "Full time", 96000, 186000, "Regular/Permanent", + ("human resources, business, or related area", 5, 7, 3), None), + ("11003", "Full time", 100000, 195000, "Regular/Permanent", + ("industrial relations, psychology, or related area", 4, 6, 2), None), + ], + }, + { + "title": "HR Business Partner", + "area": "corporate", + "category": "Human Resources", + "shifts": "WD", + "summary": "Support a client group across hiring, performance and associate relations.", + "do": [ + "HR Business Partners at {location_name} in {city}, {state} run the people cycle for their " + "client group: hiring plans, performance calibration, development and associate relations " + "cases.", + "You will coach managers who are new to leading people and hold the line on policy when it " + "matters.", + ], + "about_team": "This team supports several hundred associates across the site.", + "min_qual_option1": "Option 1: Bachelor's degree in {degree_field} and {y1} years' experience in " + "human resources or related area.", + "min_qual_option2": "Option 2: {y2} years' experience in human resources or related area.", + "preferred": "Experience with associate relations investigations for {yp} years or more.", + "placements": [ + ("11500", "Full time", 80000, 150000, "Regular/Permanent", + ("human resources or related area", 3, 5, 2), None), + ("11109", "Full time", 78000, 146000, "Regular/Permanent", + ("business administration or related area", 2, 4, 1), None), + ("12200", "Full time", 82000, 152000, "Regular/Permanent", + ("organizational psychology, human resources, or related area", 4, 6, 2), None), + ], + }, + { + "title": "Manager, Marketing", + "area": "corporate", + "category": "Marketing and Advertising", + "shifts": "WD", + "summary": "Own campaign strategy and execution for a product line.", + "do": [ + "Marketing Managers at {location_name} in {city}, {state} own the plan for a product line: " + "positioning, campaign calendar, agency briefs and the results readout.", + "You will work with creative, media and analytics, and you will be accountable for what the " + "spend returned.", + ], + "about_team": "Marketing here works close to the product teams and the sales calendar.", + "min_qual_option1": "Option 1: Bachelor's degree in {degree_field} and {y1} years' experience in " + "marketing or related area.", + "min_qual_option2": "Option 2: {y2} years' experience in marketing or related area.", + "preferred": "Experience running integrated consumer campaigns for {yp} years or more.", + "placements": [ + ("12200", "Full time", 85000, 160000, "Regular/Permanent", + ("marketing, communications, or related area", 3, 5, 2), None), + ("10101", "Full time", 90000, 170000, "Regular/Permanent", + ("marketing, business, or related area", 4, 6, 3), None), + ], + }, + { + "title": "Senior Manager, Brand Marketing", + "area": "corporate", + "category": "Marketing and Advertising", + "shifts": "WD", + "summary": "Lead brand strategy and the campaigns that carry it.", + "do": [ + "Senior Managers of Brand Marketing at {location_name} in {city}, {state} own how the brand " + "shows up: the platform, the creative standard and the campaigns that put it in front of " + "customers.", + "You will lead a small team and manage agency partners against a real budget.", + ], + "about_team": "Brand marketing sets the standard the rest of marketing works to.", + "min_qual_option1": "Option 1: Bachelor's degree in {degree_field} and {y1} years' experience in " + "brand or consumer marketing.", + "min_qual_option2": "Option 2: {y2} years' experience in brand or consumer marketing.", + "preferred": "Master's degree in business administration and {yp} years' experience managing " + "agency relationships.", + "placements": [ + ("10101", "Full time", 110000, 210000, "Regular/Permanent", + ("marketing, advertising, or related area", 6, 8, 3), None), + ("11500", "Full time", 105000, 200000, "Regular/Permanent", + ("communications, marketing, or related area", 5, 7, 2), None), + ("12200", "Full time", 102000, 196000, "Regular/Permanent", + ("brand management, marketing, or related area", 4, 6, 2), None), + ], + }, + { + "title": "Marketing Specialist III", + "area": "corporate", + "category": "Marketing and Advertising", + "shifts": "WD", + "summary": "Execute member marketing programs and report on what they returned.", + "do": [ + "Marketing Specialists at {location_name} in {city}, {state} execute the member marketing " + "calendar: briefs, asset trafficking, channel setup and post-campaign reporting.", + "You will keep several campaigns moving at once and be the person who notices the detail " + "everyone else missed.", + ], + "about_team": "Member marketing owns how the club talks to its members between visits.", + "min_qual_option1": "Option 1: Bachelor's degree in {degree_field} and {y1} years' experience in " + "marketing or related area.", + "min_qual_option2": "Option 2: {y2} years' experience in marketing or related area.", + "preferred": "Experience with customer relationship management platforms for {yp} years or more.", + "placements": [ + ("11109", "Full time", 65000, 120000, "Regular/Permanent", + ("marketing or related area", 2, 4, 1), None), + ("12200", "Full time", 68000, 126000, "Regular/Permanent", + ("marketing, media studies, or related area", 3, 5, 2), None), + ], + }, + { + "title": "Senior Buyer", + "area": "corporate", + "category": "Merchandising", + "shifts": "WD", + "summary": "Own assortment, cost and supplier relationships for a category.", + "do": [ + "Senior Buyers at {location_name} in {city}, {state} own a category: what we carry, what we " + "pay for it, and how it performs on the floor.", + "You will negotiate with suppliers, build the assortment plan by season, and answer for the " + "category's sales and margin every month.", + ], + "about_team": "Merchandising decides what ends up on the shelf and at what price.", + "min_qual_option1": "Option 1: Bachelor's degree in {degree_field} and {y1} years' experience in " + "merchandising, buying or related area.", + "min_qual_option2": "Option 2: {y2} years' experience in merchandising, buying or related area.", + "preferred": "Experience negotiating national supplier agreements for {yp} years or more.", + "placements": [ + ("10101", "Full time", 95000, 185000, "Regular/Permanent", + ("business, merchandising, or related area", 5, 7, 3), None), + ("11109", "Full time", 92000, 178000, "Regular/Permanent", + ("supply chain, business, or related area", 4, 6, 2), None), + ], + }, + { + "title": "Merchandising Manager", + "area": "corporate", + "category": "Merchandising", + "shifts": "WD", + "summary": "Turn category strategy into the plan the stores and clubs actually execute.", + "do": [ + "Merchandising Managers at {location_name} in {city}, {state} translate category strategy into " + "modulars, promotions and in-club execution plans.", + "You will work with buyers, replenishment and field leadership to make sure the plan survives " + "contact with the sales floor.", + ], + "about_team": "This team bridges the buying office and the buildings.", + "min_qual_option1": "Option 1: Bachelor's degree in {degree_field} and {y1} years' experience in " + "merchandising or related area.", + "min_qual_option2": "Option 2: {y2} years' experience in merchandising or related area.", + "preferred": "Field retail experience and {yp} years' experience with space planning tools.", + "placements": [ + ("10101", "Full time", 88000, 170000, "Regular/Permanent", + ("merchandising, business, or related area", 4, 6, 2), None), + ("11109", "Full time", 85000, 165000, "Regular/Permanent", + ("retail management, business, or related area", 3, 5, 2), None), + ], + }, + { + "title": "Replenishment Manager", + "area": "corporate", + "category": "Merchandising", + "shifts": "WD", + "summary": "Own in-stock and inventory turns for a category across the network.", + "do": [ + "Replenishment Managers at {location_name} in {city}, {state} own in-stock, forecast accuracy " + "and inventory turns for their categories across the whole network.", + "You will tune forecasting parameters, work supplier lead times, and be the first call when a " + "category goes out of stock in a region.", + ], + "about_team": "Replenishment keeps thousands of buildings full without drowning them in " + "inventory.", + "min_qual_option1": "Option 1: Bachelor's degree in {degree_field} and {y1} years' experience in " + "replenishment, supply chain or related area.", + "min_qual_option2": "Option 2: {y2} years' experience in replenishment, supply chain or related " + "area.", + "preferred": "Experience with demand forecasting systems for {yp} years or more.", + "placements": [ + ("10101", "Full time", 84000, 162000, "Regular/Permanent", + ("supply chain, industrial engineering, or related area", 4, 6, 2), None), + ], + }, + { + "title": "Senior Manager, Delivery Search, Arrival & Matching (Last Mile Delivery)", + "area": "corporate", + "category": "Business Operations", + "shifts": "WD,SD", + "summary": "Own the operations behind driver matching and arrival accuracy for last mile delivery.", + "do": [ + "Senior Managers on Last Mile Delivery at {location_name} in {city}, {state} own the " + "operational levers behind delivery search, driver arrival and order matching: the policies, " + "the thresholds and the escalation paths that keep deliveries on time.", + "You will work with product and data science on where the model ends and operations begins, " + "and you will own the metric either way.", + ], + "about_team": "Last Mile Delivery moves millions of orders from the building to the doorstep.", + "min_qual_option1": "Option 1: Bachelor's degree in {degree_field} and {y1} years' experience in " + "operations management or related area.", + "min_qual_option2": "Option 2: {y2} years' experience in operations management or related area.", + "preferred": "Master's degree in {degree_field} and {yp} years' experience in last mile or " + "transportation operations.", + "placements": [ + ("10101", "Full time", 110000, 220000, "Regular/Permanent", + ("supply chain management, operations, or related area", 6, 9, 3), + {"job_id": "R-2456729", + "qual_clause": "including experience running a last mile delivery or courier network"}), + ("11003", "Full time", 117000, 234000, "Regular/Permanent", + ("industrial engineering, logistics, or related area", 4, 6, 2), + {"qual_clause": "including experience with driver dispatch or arrival-time modeling"}), + ], + }, + { + "title": "Manager, Supply Chain Operations", + "area": "corporate", + "category": "Business Operations", + "shifts": "WD", + "summary": "Run network planning and continuous improvement for a supply chain region.", + "do": [ + "Managers of Supply Chain Operations at {location_name} in {city}, {state} own network " + "planning, cost-to-serve analysis and continuous improvement projects for their region.", + "You will spend time in the buildings, not only in the model, and you will bring changes back " + "that the operators can actually run.", + ], + "about_team": "Supply chain operations connects the network plan to what happens on the dock.", + "min_qual_option1": "Option 1: Bachelor's degree in {degree_field} and {y1} years' experience in " + "supply chain or operations.", + "min_qual_option2": "Option 2: {y2} years' experience in supply chain or operations.", + "preferred": "Lean or Six Sigma certification and {yp} years' experience leading improvement " + "projects.", + "placements": [ + ("11500", "Full time", 82000, 158000, "Regular/Permanent", + ("supply chain management or related area", 3, 5, 2), None), + ("10101", "Full time", 86000, 166000, "Regular/Permanent", + ("industrial engineering, logistics, or related area", 4, 6, 2), None), + ("12200", "Full time", 88000, 168000, "Regular/Permanent", + ("operations management, logistics, or related area", 2, 4, 1), None), + ], + }, + { + "title": "Business Operations Analyst III", + "area": "corporate", + "category": "Business Operations", + "shifts": "WD", + "summary": "Turn operational data into the decisions the business runs on.", + "do": [ + "Business Operations Analysts at {location_name} in {city}, {state} build the reporting, run " + "the analysis and write the recommendation that leadership acts on.", + "You will be trusted with the numbers, which means you will be the one who has to catch the " + "mistake in them.", + ], + "about_team": "Business operations supports planning and performance management across the site.", + "min_qual_option1": "Option 1: Bachelor's degree in {degree_field} and {y1} years' experience in " + "business analysis or related area.", + "min_qual_option2": "Option 2: {y2} years' experience in business analysis or related area.", + "preferred": "Advanced SQL and visualization experience for {yp} years or more.", + "placements": [ + ("11500", "Full time", 66000, 122000, "Regular/Permanent", + ("business analytics, economics, or related area", 2, 4, 1), None), + ("12200", "Full time", 70000, 128000, "Regular/Permanent", + ("operations analytics, business, or related area", 3, 5, 2), None), + ], + }, + { + "title": "Merchandising Intern", + "area": "students", + "category": "Internship", + "shifts": "WD", + "summary": "A paid summer internship inside a buying office, owning a real category project.", + "do": [ + "Merchandising Interns at {location_name} in {city}, {state} join a buying team for the summer " + "and own one category project end to end, from the data pull to the recommendation.", + "You will sit in supplier meetings, walk buildings with the field team, and present your " + "recommendation to merchandising leadership at the end of the program.", + ], + "about_team": "The internship program places students directly on the teams that make the " + "decisions.", + "min_qual_option1": "Option 1: Currently enrolled in a bachelor's degree program in {degree_field} " + "with an expected graduation date within {y1} years.", + "min_qual_option2": "Option 2: Currently enrolled in a master's degree program in {degree_field} " + "with an expected graduation date within {y2} years.", + "preferred": "Coursework or prior internship experience in retail merchandising within the last " + "{yp} years.", + "placements": [ + ("11109", "Intern", 64000, 90000, "Intern (Fixed Term)", + ("business, marketing, or supply chain", 2, 1, 2), None), + ("10101", "Intern", 66000, 92000, "Intern (Fixed Term)", + ("business administration or merchandising", 2, 1, 1), None), + ("11500", "Intern", 62000, 88000, "Intern (Fixed Term)", + ("business, merchandising, or analytics", 1, 2, 1), None), + ], + }, + { + "title": "Software Engineering Intern", + "area": "students", + "category": "Internship", + "shifts": "WD", + "summary": "A paid summer internship writing production code on a platform team.", + "do": [ + "Software Engineering Interns at {location_name} in {city}, {state} join a platform team, take " + "a real ticket in week one, and ship code to production before the summer is over.", + "You will have an engineering mentor, take part in code review, and present your project at " + "the end of the program.", + ], + "about_team": "Interns join the same teams and the same rituals as full-time engineers.", + "min_qual_option1": "Option 1: Currently enrolled in a bachelor's degree program in {degree_field} " + "with an expected graduation date within {y1} years.", + "min_qual_option2": "Option 2: Currently enrolled in a master's degree program in {degree_field} " + "with an expected graduation date within {y2} years.", + "preferred": "Coursework in data structures and algorithms and {yp} prior software internship.", + "placements": [ + ("11807", "Intern", 78000, 104000, "Intern (Fixed Term)", + ("computer science or computer engineering", 2, 1, 1), None), + ("10101", "Intern", 72000, 98000, "Intern (Fixed Term)", + ("computer science or information systems", 1, 2, 1), None), + ("11003", "Intern", 76000, 102000, "Intern (Fixed Term)", + ("software engineering or computer engineering", 3, 2, 1), None), + ], + }, + { + "title": "Finance Intern", + "area": "students", + "category": "Internship", + "shifts": "WD", + "summary": "A paid summer internship on a finance planning team.", + "do": [ + "Finance Interns at {location_name} in {city}, {state} support a planning team through a full " + "forecast cycle and own one analysis that goes in front of a business leader.", + "You will learn the planning system, the reporting stack and how the company actually decides " + "where money goes.", + ], + "about_team": "Finance interns sit with the teams they support, not in a separate cohort room.", + "min_qual_option1": "Option 1: Currently enrolled in a bachelor's degree program in {degree_field} " + "with an expected graduation date within {y1} years.", + "min_qual_option2": "Option 2: Currently enrolled in a master's degree program in {degree_field} " + "with an expected graduation date within {y2} years.", + "preferred": "Coursework in financial modelling and {yp} prior finance internship.", + "placements": [ + ("10101", "Intern", 60000, 84000, "Intern (Fixed Term)", + ("finance, accounting, or economics", 2, 1, 1), None), + ("11500", "Intern", 58000, 82000, "Intern (Fixed Term)", + ("accounting or business administration", 1, 2, 1), None), + ], + }, + { + "title": "Data Analytics Intern", + "area": "students", + "category": "Internship", + "shifts": "WD", + "summary": "A paid summer internship on an analytics team supporting eCommerce.", + "do": [ + "Data Analytics Interns at {location_name} in {city}, {state} join an analytics team, build a " + "dashboard or model that a business owner asked for, and hand it over working.", + "You will use SQL and Python daily and present your findings at the end of the program.", + ], + "about_team": "Analytics here reports into the product organisation it supports.", + "min_qual_option1": "Option 1: Currently enrolled in a bachelor's degree program in {degree_field} " + "with an expected graduation date within {y1} years.", + "min_qual_option2": "Option 2: Currently enrolled in a master's degree program in {degree_field} " + "with an expected graduation date within {y2} years.", + "preferred": "Coursework in statistics or machine learning and {yp} prior analytics internship.", + "placements": [ + ("11003", "Intern", 70000, 96000, "Intern (Fixed Term)", + ("statistics, data science, or economics", 2, 1, 1), None), + ("11807", "Intern", 74000, 100000, "Intern (Fixed Term)", + ("computer science, statistics, or mathematics", 1, 3, 1), None), + ], + }, +] + + +# --------------------------------------------------------------------------- # +# Hub copy for the four is_hub offices, keyed by store number: +# (display name, blurb, image file). Seeded onto Store.hub_name / hub_blurb / +# hub_image so the locations and career-area templates read it from the DB. +# --------------------------------------------------------------------------- # +HUB_COPY = { + "10101": ( + "Northwest Arkansas", + "Northwest Arkansas offers trails, local eats, and the Crystal Bridges Museum - while our " + "12 new Home Office buildings reflect the company's story through thoughtful design.", + "loc-nwa.jpg", + ), + "11807": ( + "Sunnyvale", + "A weekend hike through the mountains. An evening walk next to the ocean. A quick visit to a " + "museum. The best of both worlds - work and leisure - are waiting for you right here.", + "loc-sunnyvale.jpg", + ), + "11003": ( + "Hoboken", + "Just across from Lower Manhattan, Hoboken is a walkable, character-filled town on the Hudson " + "with a truly unique charm.", + "loc-hoboken.jpg", + ), + "11500": ( + "Dallas", + "Our Dallas office anchors merchandising, finance and supply chain teams in the middle of one " + "of the fastest-growing metros in the country.", + "loc-dallas.jpg", + ), +} + + +# --------------------------------------------------------------------------- # +# Job ids surfaced as "Trending roles" on the home page, the hiring page and the +# logged-out saved-roles page. Seeded onto Job.is_trending, which is what the +# handlers query. These three placements pin their job_id in `extras` so a +# catalog edit cannot silently point this list at a different posting. +# --------------------------------------------------------------------------- # +TRENDING_JOB_IDS = [ + "R-2418512", + "R-2417063", + "CP-1236-10741", +] + + +# --------------------------------------------------------------------------- # +# "What you'll bring" bullets for salaried postings, one template list per +# category. Slot fills are location only ({city}, {state}, {location_name}); the +# bullets deliberately never mention a degree or a number of years, which live +# solely in the Minimum Qualifications block. +# --------------------------------------------------------------------------- # +SALARIED_BRING = { + "Software Engineering and Architecture": [ + "A track record of designing, building and operating production services that hold up at retail traffic.", + "Fluency in at least one modern backend language and its ecosystem, plus comfort reading code in others.", + "Hands-on experience with distributed data stores, message queues and cloud infrastructure.", + "A test-driven approach to development and a strong commitment to code quality and documentation.", + "Clear written and spoken communication with engineers, product managers and partners across {city}.", + ], + "Product Management": [ + "Experience owning a product area end to end, from discovery through launch and iteration.", + "The ability to turn ambiguous customer problems into a crisp roadmap and measurable outcomes.", + "Comfort working daily with engineering, design and data science partners in {city}.", + "Strong written communication, including product specs and executive updates.", + ], + "Data Science and Analytics": [ + "Hands-on experience building and shipping statistical or machine learning models in production.", + "Fluency in Python or R and SQL, and comfort working with very large datasets.", + "The judgment to know when a simple model beats a complex one.", + "Experience explaining findings to non-technical partners across the {city} office.", + ], + "Information Security": [ + "Deep familiarity with threat modeling, secure design review and incident response.", + "Experience with identity, access management and cloud security controls at scale.", + "The ability to translate risk into priorities that engineering teams can act on.", + "Calm, clear communication during live incidents.", + ], + "Creative Design and UX": [ + "A portfolio that shows end-to-end design work, from research through shipped experience.", + "Fluency in modern design and prototyping tools and a working knowledge of front-end constraints.", + "Experience planning and running user research and turning it into design decisions.", + "The ability to present and defend design decisions to partners in {city}.", + ], + "Technical Program Management": [ + "Experience running large cross-functional programs with many engineering teams.", + "Enough technical depth to challenge estimates and spot dependencies early.", + "A bias for clear plans, visible risks and honest status.", + "Strong facilitation skills across the {city} office and remote partners.", + ], + "Information Technology": [ + "Experience supporting enterprise endpoints, identity systems and collaboration tools.", + "Scripting skills for automating repetitive support and provisioning tasks.", + "A customer-first approach to troubleshooting and a habit of documenting fixes.", + "Comfort supporting associates on site in {city} and remotely.", + ], + "Accounting and Finance": [ + "Experience owning forecasts, budgets or close processes for a large business unit.", + "Advanced spreadsheet and financial modeling skills, plus comfort with planning systems.", + "The ability to explain variances to operators and executives in plain language.", + "Attention to detail and a strong sense of ownership over the numbers.", + ], + "Human Resources": [ + "Experience partnering with leaders on talent, organization design and associate relations.", + "Working knowledge of employment practices and the judgment to apply them fairly.", + "Strong coaching and facilitation skills.", + "Comfort supporting teams across the {city} office and the field.", + ], + "Marketing and Advertising": [ + "Experience planning and running integrated campaigns across digital and in-store channels.", + "Fluency in campaign measurement and the ability to act on what the data says.", + "Strong creative judgment and clear briefing skills for agency and in-house partners.", + "Comfort presenting plans and results to senior leaders in {city}.", + ], + "Merchandising": [ + "Experience owning assortment, pricing or replenishment decisions for a category.", + "Strong analytical skills and comfort working in large planning and forecasting systems.", + "The ability to negotiate with suppliers and build long-term partnerships.", + "A customer-first mindset and a habit of walking the stores.", + ], + "Business Operations": [ + "Experience owning operational metrics and the processes behind them.", + "Strong analytical skills, including the ability to build and interpret operational dashboards.", + "Comfort working across product, data science and field operations partners.", + "A habit of spending time where the work happens, not only in the model.", + ], + "Internship": [ + "Current enrollment in a degree program with an expected graduation date after the internship term.", + "Curiosity about how a large retailer runs and a willingness to ask questions.", + "Comfort working in a team and presenting your project to leaders in {city}.", + "Availability for the full internship term.", + ], +} diff --git a/sites/walmart_careers/check_tracked_assets.py b/sites/walmart_careers/check_tracked_assets.py new file mode 100644 index 00000000..cda25b91 --- /dev/null +++ b/sites/walmart_careers/check_tracked_assets.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Validate tracked Walmart Careers icon/font bytes and provenance metadata.""" +from __future__ import annotations + +import hashlib +import json +from pathlib import Path, PurePosixPath +from urllib.parse import urlsplit + +SITE = Path(__file__).resolve().parent +MANAGED_ROOTS = ("static/icons", "static/fonts") + + +def verify() -> int: + manifest = json.loads((SITE / "tracked_asset_inventory.json").read_text()) + rows = manifest.get("assets") + if manifest.get("schema_version") != 1 or not isinstance(rows, list): + raise ValueError("unsupported tracked asset inventory") + expected = set() + for row in rows: + relative = PurePosixPath(row["path"]) + if relative.is_absolute() or ".." in relative.parts or not any( + row["path"].startswith(root + "/") for root in MANAGED_ROOTS + ): + raise ValueError(f"unsafe tracked asset path: {row['path']!r}") + expected.add(row["path"]) + data = (SITE / row["path"]).read_bytes() + if len(data) != row["bytes"] or hashlib.sha256(data).hexdigest() != row["sha256"]: + raise ValueError(f"tracked asset mismatch: {row['path']}") + source = urlsplit(row["source_url"]) + if source.scheme != "https" or not source.hostname: + raise ValueError(f"invalid source URL: {row['path']}") + actual = { + str(path.relative_to(SITE)) + for root in MANAGED_ROOTS + for path in (SITE / root).iterdir() + if path.is_file() and path.name != ".gitkeep" + } + if len(expected) != len(rows) or expected != actual or manifest.get("asset_count") != len(rows): + raise ValueError(f"tracked inventory mismatch: missing={sorted(expected-actual)} extra={sorted(actual-expected)}") + return len(rows) + + +if __name__ == "__main__": + print(f"[check] verified {verify()} tracked Walmart Careers assets") diff --git a/sites/walmart_careers/provenance.json b/sites/walmart_careers/provenance.json new file mode 100644 index 00000000..b7e5c3dc --- /dev/null +++ b/sites/walmart_careers/provenance.json @@ -0,0 +1,32 @@ +{ + "schema_version": 1, + "snapshot_date": "2026-08-31", + "records": [ + { + "path": "catalog_source.py", + "classification": "synthetic", + "scope": "All job postings, requisition identifiers, stores, addresses, coordinates, pay ranges, shifts, qualification text and task distractors are deterministic benchmark data. They are not Walmart vacancies or real application records." + }, + { + "path": "seed_data.py", + "classification": "synthetic", + "scope": "Benchmark users, saved roles and applications are synthetic fixtures. Runtime state is reset from the generated seed." + }, + { + "path": "_content.py", + "classification": "adapted-and-synthetic", + "source_url": "https://careers.walmart.com/us/en/home", + "scope": "Navigation labels and visual structure are adapted from the source site. Marketing, hiring, benefits, location and testimonial prose is frozen mirror copy and must not be treated as current Walmart policy." + }, + { + "path": "asset_inventory.json", + "classification": "source-backed-media-inventory", + "scope": "Every HF-managed runtime image has a local byte count and SHA-256. Direct source asset URLs are recorded where independently recovered; source_page entries explicitly identify the remaining missing direct-asset evidence." + }, + { + "path": "tracked_asset_inventory.json", + "classification": "source-backed-media-inventory", + "scope": "Every tracked runtime icon and font has a local byte count, SHA-256 and source URL classification." + } + ] +} diff --git a/sites/walmart_careers/requirements.txt b/sites/walmart_careers/requirements.txt new file mode 100644 index 00000000..02bbaea7 --- /dev/null +++ b/sites/walmart_careers/requirements.txt @@ -0,0 +1,7 @@ +Flask==3.1.0 +Flask-SQLAlchemy==3.1.1 +Flask-Login==0.6.3 +Flask-WTF==1.2.2 +Werkzeug==3.1.3 +SQLAlchemy==2.0.36 +Pillow==11.0.0 diff --git a/sites/walmart_careers/seed_data.py b/sites/walmart_careers/seed_data.py new file mode 100644 index 00000000..b0f63e79 --- /dev/null +++ b/sites/walmart_careers/seed_data.py @@ -0,0 +1,539 @@ +"""Deterministic seed for the Walmart Careers mirror. + +Run directly (`PYTHONHASHSEED=0 python seed_data.py`) to rebuild +`instance_seed/walmart_careers.db` from `catalog_source.py`. The build is +byte-reproducible: one RNG, no wall-clock reads, sorted iteration only, and +werkzeug password hashes hard-coded because werkzeug salts randomly. +""" +from __future__ import annotations + +import importlib.util +import os +import random +import shutil +from datetime import datetime, timedelta +from pathlib import Path + +from sqlalchemy import text + +os.environ.setdefault("WEBSYN_SKIP_BOOTSTRAP", "1") + +import catalog_source as source +from _content import MIRROR_REFERENCE_DATE +from app import ( + Application, + ApplicationDraft, + Area, + Category, + Job, + SavedJob, + SeedMetadata, + Store, + User, + SEED_VERSION, + app, + confirmation_for, + db, + dumps_json, +) + +RNG = random.Random(20260905) +BASE_DIR = Path(__file__).resolve().parent +DB_PATH = BASE_DIR / "instance" / "walmart_careers.db" +INSTANCE_SEED_DIR = BASE_DIR / "instance_seed" + +# Hard-coded werkzeug hashes of DEMO_PASSWORD ("TestPass123!"). generate_password_hash +# salts randomly, so recomputing them here would break byte-identical rebuilds. +DEMO_PASSWORD_HASHES = { + "alice.j@test.com": "scrypt:32768:8:1$x9JMG7iKsrRO1AGh$e8e195799326a6e1ff55d4d20dd2735d9d68c0ed4879bd6b263ac33fa9953b0f6c8505680f1a1d8a9fbe9b0d01ef5e88f99b77b89fc30299fda217185a3b7acf", + "bob.c@test.com": "scrypt:32768:8:1$O14LIVdpqb3Q6D7E$4b9389cd00aad4417058fd629af5bf979b3f05bdf011791fbc65b7080fe898e50c7aedc2c22be92c71ae25a1df6922bb4ca44b386a7f17040b36888c0cdc8942", + "carol.d@test.com": "scrypt:32768:8:1$9PGtFS6I89BOEugS$890b1c1935bb6c0c4a6f7f5ad689cc02415e4bd03b02e101f0c2095931d4f157a8504c0fa4f12c3073c94e1480fea3305ffbadc5e8540c5eaf1c16965cb47be7", + "david.k@test.com": "scrypt:32768:8:1$luqs3gbiT1hPpw2c$09f31fef9514ae90d234cdd91a7f2c95d937e08a37fed60ce0b41755a097609040f7aa5083b94ecdd41804262dc778bd6f8d8e9f38f47f319fa8d6f3ed705d1b", +} + +BENCHMARK_USERS = [ + ("alice.j@test.com", "alice.j", "Alice Johnson", "Alice", "Johnson", "479-555-0134", "Bentonville", "AR"), + ("bob.c@test.com", "bob.c", "Bob Chen", "Bob", "Chen", "206-555-0178", "Seattle", "WA"), + ("carol.d@test.com", "carol.d", "Carol Davis", "Carol", "Davis", "253-555-0119", "Tacoma", "WA"), + ("david.k@test.com", "david.k", "David Kim", "David", "Kim", "214-555-0166", "Dallas", "TX"), +] +USER_CREATED_AT = datetime(2026, 6, 12, 9, 30, 0) +EXPECTED_COUNTS = { + "areas": 7, + "categories": 33, + "stores": 51, + "jobs": 246, + "users": 4, + "saved_jobs": 13, + "applications": 4, + "application_drafts": 0, +} + +# (user email, job title, store number) — resolved to job ids after the catalog is built. +SEED_SAVED_JOBS = [ + ("alice.j@test.com", "Freight Handler", "9046", datetime(2026, 8, 3, 14, 12, 0)), + ("alice.j@test.com", "Cosmetics Cashier", "2503", datetime(2026, 8, 7, 19, 41, 0)), + ("alice.j@test.com", "Optician", "5991", datetime(2026, 8, 9, 10, 5, 0)), + ("alice.j@test.com", "Automation Technician", "6088", datetime(2026, 8, 13, 8, 16, 0)), + ("alice.j@test.com", "Senior UX Designer", "11807", datetime(2026, 8, 17, 12, 27, 0)), + ("alice.j@test.com", "Team Lead", "4137", datetime(2026, 8, 22, 17, 58, 0)), + ("bob.c@test.com", "Asset Protection Associate", "5991", datetime(2026, 8, 4, 8, 22, 0)), + ("bob.c@test.com", "Class A CDL Truck Driver", "6038", datetime(2026, 8, 11, 16, 48, 0)), + ("bob.c@test.com", "Senior Data Scientist", "11500", datetime(2026, 8, 20, 12, 3, 0)), + ("carol.d@test.com", "Pharmacy Technician", "4137", datetime(2026, 8, 6, 7, 55, 0)), + ("carol.d@test.com", "Cafe Associate", "6318", datetime(2026, 8, 14, 21, 17, 0)), + ("david.k@test.com", "Merchandising and Stocking Associate", "4750", datetime(2026, 8, 8, 11, 26, 0)), + ("david.k@test.com", "IT Support Engineer", "10101", datetime(2026, 8, 19, 15, 34, 0)), +] + +# (user email, job title, store number, submitted_at) +SEED_APPLICATIONS = [ + ("alice.j@test.com", "Team Lead", "144", datetime(2026, 8, 5, 13, 20, 0)), + ("bob.c@test.com", "Order Filler", "6014", datetime(2026, 8, 12, 9, 2, 0)), + ("carol.d@test.com", "Optician", "2073", datetime(2026, 8, 16, 17, 44, 0)), + ("david.k@test.com", "Financial Analyst III", "11500", datetime(2026, 8, 21, 10, 11, 0)), +] + +HERO_SETS = { + "salaried": ["jobhero-corp-1.jpg", "jobhero-corp-2.jpg", "jobhero-corp-3.jpg"], + "sams": ["jobhero-sams-1.png", "jobhero-sams-2.jpg", "jobhero-wm-2.jpg"], + "walmart": ["jobhero-wm-1.png", "jobhero-wm-3.jpg", "jobhero-wm-4.jpg"], + "walmart-alt": ["jobhero-wm-4.jpg", "jobhero-wm-2.jpg", "jobhero-wm-1.png"], +} + +HOURLY_CLOSING = ( + "At Walmart, we offer competitive pay as well as performance-based incentive awards and other " + "great benefits for a happier mind, body, and wallet. Health benefits include medical, vision and " + "dental coverage. Financial benefits include 401(k), stock purchase and company-paid life " + "insurance. Paid time off benefits include parental leave, family care leave, bereavement, jury " + "duty, and voting." +) +LBU_CLOSING = ( + "Live Better U is a Walmart-paid education benefit program for full-time and part-time associates " + "in Walmart and Sam's Club facilities. Programs range from high school completion to bachelor's " + "degrees, including English Language Learning and short-form certificates. Tuition, books, and " + "fees are completely paid for by Walmart." +) + + +# --------------------------------------------------------------------------- # +# Catalog construction +# --------------------------------------------------------------------------- # +def _shift_names(codes: str) -> list[str]: + return [source.SHIFT_CODES[c] for c in codes.split(",")] + + +def _build_areas() -> dict[str, Area]: + areas: dict[str, Area] = {} + for slug, name, order, blurb, hero, has_index, filterable in source.AREAS: + area = Area( + slug=slug, + name=name, + display_order=order, + blurb=blurb, + hero_image=hero, + has_index_page=has_index, + is_filterable=filterable, + ) + db.session.add(area) + areas[slug] = area + db.session.flush() + return areas + + +def _build_categories(areas: dict[str, Area]) -> dict[tuple[str, str], Category]: + categories: dict[tuple[str, str], Category] = {} + for area_slug, name, slug, order in source.CATEGORIES: + category = Category( + area_id=areas[area_slug].id, name=name, slug=slug, display_order=order + ) + db.session.add(category) + categories[(area_slug, name)] = category + db.session.flush() + return categories + + +def _build_stores() -> dict[str, Store]: + stores: dict[str, Store] = {} + for row in source.STORES: + (number, banner, location_name, street, city, state, zip_code, + lat, lng, is_hub, is_office, _brand) = row + hub_name, hub_blurb, hub_image = source.HUB_COPY.get(number, (None, None, None)) + store = Store( + store_number=number, + banner=banner, + location_name=location_name, + street=street, + city=city, + state=state, + zip=zip_code, + lat=lat, + lng=lng, + is_hub=is_hub, + is_office=is_office, + hub_name=hub_name, + hub_blurb=hub_blurb, + hub_image=hub_image, + ) + db.session.add(store) + stores[number] = store + db.session.flush() + return stores + + +def _store_brand() -> dict[str, str]: + return {row[0]: row[11] for row in source.STORES} + + +def _hero_for(population: str, brand: str, index: int) -> list[str]: + if population == "salaried": + pool = HERO_SETS["salaried"] + elif brand == "Sam's Club": + pool = HERO_SETS["sams"] + elif index % 2: + pool = HERO_SETS["walmart-alt"] + else: + pool = HERO_SETS["walmart"] + return list(pool) + + +def _build_jobs(areas, categories, stores) -> list[Job]: + brands = _store_brand() + trending = set(source.TRENDING_JOB_IDS) + used_ids: set[str] = set() + for family in source.HOURLY_FAMILIES: + for placement in family["placements"]: + extras = placement[6] or {} + if "job_id" in extras: + used_ids.add(extras["job_id"]) + for family in source.SALARIED_FAMILIES: + for placement in family["placements"]: + extras = placement[6] or {} + if "job_id" in extras: + used_ids.add(extras["job_id"]) + + jobs: list[Job] = [] + store_cursor: dict[str, int] = {} + index = 0 + + for family in source.HOURLY_FAMILIES: + area = areas[family["area"]] + category = categories[(family["area"], family["category"])] + for placement in family["placements"]: + store_no, emp_type, codes, min_pay, max_pay, positions, extras = placement + extras = extras or {} + store = stores[store_no] + brand = brands[store_no] + cursor = store_cursor.get(store_no, 10200) + RNG.randint(120, 980) + job_id = extras.get("job_id") + if job_id is None: + job_id = f"CP-{store_no}-{cursor}" + while job_id in used_ids: + cursor += 37 + job_id = f"CP-{store_no}-{cursor}" + store_cursor[store_no] = cursor + used_ids.add(job_id) + + fmt = { + "banner": store.banner, + "store": store.store_number, + "city": store.city, + "state": store.state, + "location_name": store.location_name, + } + paragraphs = [p.format(**fmt) for p in family["do"]] + paragraphs.append(HOURLY_CLOSING) + paragraphs.append(LBU_CLOSING) + primary_code = codes.split(",")[0] + job = Job( + job_id=job_id, + population="hourly", + title=family["title"], + brand=brand, + store_id=store.id, + area_id=area.id, + category_id=category.id, + shifts_json=dumps_json(_shift_names(codes)), + employment_type=emp_type, + pay_frequency="Hourly", + min_pay=min_pay, + max_pay=max_pay, + posted_date=MIRROR_REFERENCE_DATE - timedelta(days=RNG.randint(1, 120)), + sort_rank=0, + is_trending=job_id in trending, + summary=family["summary"].format(**fmt), + description="\n\n".join(paragraphs), + additional_description_json=dumps_json( + [b.format(**fmt) for b in family["bring"]] + ), + hashtag=family.get("hashtag"), + shift_time=extras.get("shift_time", source.SHIFT_WINDOWS[primary_code]), + positions_available=positions, + min_age_note=emp_type != "Intern", + hero_images_json=dumps_json(_hero_for("hourly", brand, index)), + ) + db.session.add(job) + jobs.append(job) + index += 1 + + salaried_cursor = 2410000 + posting_seq = 5210000 + for family in source.SALARIED_FAMILIES: + area = areas[family["area"]] + category = categories[(family["area"], family["category"])] + for placement in family["placements"]: + store_no, emp_type, min_pay, max_pay, worker_type, slots, extras = placement + extras = extras or {} + store = stores[store_no] + brand = brands[store_no] + salaried_cursor += RNG.randint(150, 900) + job_id = extras.get("job_id") + if job_id is None: + job_id = f"R-{salaried_cursor}" + while job_id in used_ids: + salaried_cursor += 13 + job_id = f"R-{salaried_cursor}" + used_ids.add(job_id) + posting_seq += RNG.randint(400, 4000) + + degree_field, y1, y2, yp = slots + fmt = { + "banner": store.banner, + "store": store.store_number, + "city": store.city, + "state": store.state, + "location_name": store.location_name, + "degree_field": degree_field, + "y1": y1, + "y2": y2, + "yp": yp, + } + paragraphs = [p.format(**fmt) for p in family["do"]] + clause = extras.get("qual_clause") + + def qualification(template: str) -> str: + text = template.format(**fmt) + if clause: + text = text.rstrip(".") + ", " + clause + "." + return text + + bring = [b.format(**fmt) for b in source.SALARIED_BRING[family["category"]]] + job = Job( + job_id=job_id, + population="salaried", + title=family["title"], + brand=brand, + store_id=store.id, + area_id=area.id, + category_id=category.id, + shifts_json=dumps_json(_shift_names(family["shifts"])), + employment_type=emp_type, + pay_frequency="Annual", + min_pay=min_pay, + max_pay=max_pay, + posted_date=MIRROR_REFERENCE_DATE - timedelta(days=RNG.randint(1, 120)), + sort_rank=0, + is_trending=job_id in trending, + summary=family["summary"].format(**fmt), + description="\n\n".join(paragraphs), + about_team=family["about_team"].format(**fmt), + additional_description_json=dumps_json(bring), + hashtag=None, + shift_time=None, + positions_available=None, + min_age_note=False, + worker_type=worker_type, + job_posting_id=f"JOB_POSTING-3-{posting_seq}", + min_qualifications_json=dumps_json( + [ + qualification(family["min_qual_option1"]), + qualification(family["min_qual_option2"]), + ] + ), + preferred_qualifications=family["preferred"].format(**fmt), + hero_images_json=dumps_json(_hero_for("salaried", brand, index)), + ) + db.session.add(job) + jobs.append(job) + index += 1 + + ranks = list(range(len(jobs))) + RNG.shuffle(ranks) + for job, rank in zip(jobs, ranks): + job.sort_rank = rank + db.session.flush() + return jobs + + +# --------------------------------------------------------------------------- # +# Seed entry points +# --------------------------------------------------------------------------- # +def seed_database(force: bool = False) -> None: + if Job.query.count() > 0 and not force: + return + RNG.seed(20260905) + areas = _build_areas() + categories = _build_categories(areas) + stores = _build_stores() + _build_jobs(areas, categories, stores) + + +def seed_benchmark_users(force: bool = False) -> None: + if User.query.count() > 0 and not force: + return + users: dict[str, User] = {} + for email, username, display, first, last, phone, city, state in BENCHMARK_USERS: + user = User( + email=email, + username=username, + display_name=display, + first_name=first, + last_name=last, + phone=phone, + city=city, + state=state, + password_hash=DEMO_PASSWORD_HASHES[email], + created_at=USER_CREATED_AT, + ) + db.session.add(user) + users[email] = user + db.session.flush() + + for email, title, store_number, saved_at in SEED_SAVED_JOBS: + job = _find_job(title, store_number) + db.session.add(SavedJob(user_id=users[email].id, job_id=job.job_id, saved_at=saved_at)) + + for email, title, store_number, submitted_at in SEED_APPLICATIONS: + job = _find_job(title, store_number) + user = users[email] + application = Application( + job_id=job.job_id, + user_id=user.id, + email=user.email, + first_name=user.first_name, + last_name=user.last_name, + phone=user.phone, + status="Submitted", + confirmation_no="pending", + submitted_at=submitted_at, + ) + db.session.add(application) + db.session.flush() + application.confirmation_no = confirmation_for(application.id) + + + +def _find_job(title: str, store_number: str) -> Job: + store = Store.query.filter_by(store_number=store_number).one() + job = ( + Job.query.filter_by(title=title, store_id=store.id) + .order_by(Job.job_id) + .first() + ) + if job is None: + raise RuntimeError(f"no seeded job {title!r} at store {store_number}") + return job + + +# --------------------------------------------------------------------------- # +# Build-time invariant checks. +# +# The benchmark task locators and their expected answers live in +# scripts_dev/assert_distractors.py, which is git-ignored and docker-ignored and +# therefore absent from the shipped tree. The freezer loads it by path when it is +# there, so a developer rebuild still fails on a catalog edit that breaks a task; +# a tree without it builds the same database and skips the checks. +# --------------------------------------------------------------------------- # +def _load_distractor_checks(): + path = BASE_DIR / "scripts_dev" / "assert_distractors.py" + if not path.exists(): + return None + spec = importlib.util.spec_from_file_location("walmart_careers_assert_distractors", path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module.assert_distractors + + +def _current_counts() -> dict[str, int]: + return { + "areas": Area.query.count(), + "categories": Category.query.count(), + "stores": Store.query.count(), + "jobs": Job.query.count(), + "users": User.query.count(), + "saved_jobs": SavedJob.query.count(), + "applications": Application.query.count(), + "application_drafts": ApplicationDraft.query.count(), + } + + +def _seed_is_complete() -> bool: + marker = db.session.get(SeedMetadata, "version") + counts = _current_counts() + core_counts_match = all(counts[key] == EXPECTED_COUNTS[key] for key in ("areas", "categories", "stores", "jobs")) + benchmark_emails = {email for (email, *_rest) in BENCHMARK_USERS} + present_emails = {row.email for row in User.query.filter(User.email.in_(benchmark_emails)).all()} + return marker is not None and marker.value == SEED_VERSION and core_counts_match and present_emails == benchmark_emails + + +def _database_has_seed_rows() -> bool: + return any(_current_counts().values()) or SeedMetadata.query.count() > 0 + + +def _validate_seed() -> None: + counts = _current_counts() + if counts != EXPECTED_COUNTS: + raise RuntimeError(f"seed row counts differ: expected={EXPECTED_COUNTS}, actual={counts}") + violations = db.session.execute(text("PRAGMA foreign_key_check")).all() + if violations: + raise RuntimeError(f"seed foreign-key violations: {violations[:5]}") + + +def ensure_seed_database() -> None: + if _seed_is_complete(): + return + if _database_has_seed_rows(): + raise RuntimeError("walmart_careers database is partial, unversioned, or from another seed version") + try: + seed_database(force=True) + seed_benchmark_users(force=True) + _validate_seed() + db.session.add(SeedMetadata(key="version", value=SEED_VERSION)) + db.session.commit() + except Exception: + db.session.rollback() + raise + + +def build_seed_database() -> None: + INSTANCE_SEED_DIR.mkdir(parents=True, exist_ok=True) + DB_PATH.parent.mkdir(parents=True, exist_ok=True) + destination = INSTANCE_SEED_DIR / "walmart_careers.db" + checks = _load_distractor_checks() + try: + with app.app_context(): + db.session.remove() + db.engine.dispose() + if DB_PATH.exists(): + DB_PATH.unlink() + with app.app_context(): + db.create_all() + ensure_seed_database() + if checks is not None: + checks() + db.session.remove() + db.engine.dispose() + temporary = destination.with_suffix(".db.tmp") + shutil.copyfile(DB_PATH, temporary) + os.replace(temporary, destination) + except Exception: + destination.with_suffix(".db.tmp").unlink(missing_ok=True) + DB_PATH.unlink(missing_ok=True) + raise + if checks is None: + print("scripts_dev/assert_distractors.py not present - tracked tests provide the release invariants.") + + +if __name__ == "__main__": + build_seed_database() + print("Seed database generated from the deterministic Walmart Careers source catalog.") diff --git a/sites/walmart_careers/static/css/site.css b/sites/walmart_careers/static/css/site.css new file mode 100644 index 00000000..e775f665 --- /dev/null +++ b/sites/walmart_careers/static/css/site.css @@ -0,0 +1,924 @@ +/* Walmart Careers mirror — Living Design tokens pulled from the live site CSS. */ +@font-face { + font-family: "EverydaySansUI"; + src: url("../fonts/EverydaySansUI-wght.ttf") format("truetype-variations"); + font-weight: 100 900; + font-display: swap; +} + +:root { + --ld-blue-100: #0053e2; + --ld-blue-130: #002e99; + --ld-blue-160: #001e60; + --ld-blue-10: #e6f1fc; + --ld-blue-9: #e9f1fe; + --ld-sky: #a9ddf7; + --ld-sky-60: #4dbdf5; + --ld-spark-100: #ffc220; + --ld-spark-30: #ffe4a3; + --ld-gray-200: #f1f1f2; + --ld-gray-20: #e3e4e5; + --ld-gray-5: #f8f8f8; + --ld-gray-100: #74767c; + --ld-text-subtle: #515357; + --ld-red: #de1c24; + --ld-green: #2a8703; + --ld-sams: #00358e; + --header-h: 80px; + --gutter: 140px; +} + +* { box-sizing: border-box; } + +html { scroll-behavior: smooth; scroll-padding-top: calc(var(--header-h) + 16px); } +[id] { scroll-margin-top: calc(var(--header-h) + 16px); } +:where(a, button, summary, input, select, textarea):focus-visible { + outline: 3px solid var(--ld-spark-100); outline-offset: 3px; +} +.header-search form:focus-within, .hero-search:focus-within, .find-search:focus-within { + outline: 3px solid var(--ld-spark-100); outline-offset: 3px; +} + +body { + margin: 0; + font-family: "EverydaySansUI", "Bogle", Arial, Helvetica, sans-serif; + font-size: 16px; + line-height: 1.45; + color: var(--ld-blue-160); + background: #fff; +} + +a { color: var(--ld-blue-100); } +a:hover { color: var(--ld-blue-130); } + +img { max-width: 100%; } + +.skip-link { + position: absolute; left: -9999px; top: 0; background: #fff; color: var(--ld-blue-160); + padding: 8px 16px; z-index: 100; +} +.skip-link:focus { left: 8px; top: 8px; } + +.wrap { max-width: 1440px; margin: 0 auto; padding: 0 32px; } +.l1 .wrap { padding: 0 var(--gutter); } +.wrap-narrow { max-width: 900px; margin: 0 auto; padding: 0 32px; } +.wrap-bleed { max-width: 1440px; margin: 0 auto; padding: 0 24px; } + +/* ------------------------------ header ---------------------------------- */ +.site-header { + background: var(--ld-blue-100); + min-height: var(--header-h); + display: flex; align-items: center; + position: sticky; top: 0; z-index: 60; +} +.site-header .wrap { + display: flex; align-items: center; gap: 20px; width: 100%; flex-wrap: nowrap; + max-width: none; padding: 0 24px; +} +.brand { display: flex; align-items: center; flex: 0 0 auto; text-decoration: none; } +.brand img { height: 38px; width: auto; } + +.main-nav { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; white-space: nowrap; } +.main-nav .nav-link, .nav-menu > summary { + color: #fff; text-decoration: none; font-size: 16px; padding: 10px 12px; + border-radius: 8px; display: inline-flex; align-items: center; gap: 6px; + cursor: pointer; list-style: none; white-space: nowrap; +} +.nav-menu > summary::-webkit-details-marker { display: none; } +.nav-menu > summary::marker { content: ""; } +.main-nav .nav-link:hover, .nav-menu > summary:hover, +.nav-menu[open] > summary { background: var(--ld-blue-130); color: #fff; } + +.nav-menu { position: relative; } +.nav-pop { + position: absolute; top: calc(100% + 10px); left: 0; z-index: 70; + background: #fff; border-radius: 16px; padding: 12px 8px; min-width: 268px; + box-shadow: 0 8px 28px rgba(0, 30, 96, .22); display: flex; flex-direction: column; +} +.nav-menu:not([open]) .nav-pop, .user-menu:not([open]) .nav-pop, .pop-wrap:not([open]) .pop-panel { display: none; } +.nav-pop a, .nav-pop .linkish { + color: var(--ld-blue-160); text-decoration: none; padding: 9px 16px; border-radius: 10px; + font-size: 15px; text-align: left; white-space: nowrap; +} +.nav-pop a:hover, .nav-pop .linkish:hover { background: var(--ld-blue-9); color: var(--ld-blue-160); } +.nav-pop hr { border: 0; border-top: 1px solid var(--ld-gray-20); margin: 8px 12px; width: calc(100% - 24px); } +.nav-pop .lang { + padding: 9px 16px; font-size: 14px; color: var(--ld-blue-160); + display: inline-flex; align-items: center; gap: 8px; +} +.nav-pop .lang svg { color: var(--ld-blue-100); } + +.header-search { display: flex; align-items: center; flex: 1 1 auto; justify-content: flex-end; } +.header-spacer { flex: 1 1 auto; } +.header-search form { + display: flex; align-items: center; background: #fff; + border-radius: 999px; padding: 5px 5px 5px 22px; + width: 100%; max-width: 448px; +} +.header-search input { + border: 0; outline: none; flex: 1; min-width: 0; font-size: 15px; font-family: inherit; + color: var(--ld-blue-160); background: transparent; +} +.header-search input::placeholder { color: var(--ld-blue-160); } +.header-search button { + border: 0; background: var(--ld-blue-100); width: 38px; height: 38px; + border-radius: 999px; cursor: pointer; display: grid; place-items: center; +} +.header-search button svg { color: #fff; } + +.user-menu { flex: 0 0 auto; position: relative; } +.user-menu > summary { + list-style: none; cursor: pointer; color: #fff; display: grid; place-items: center; + width: 40px; height: 40px; border-radius: 999px; +} +.user-menu > summary::-webkit-details-marker { display: none; } +.user-menu > summary::marker { content: ""; } +.user-menu[open] > summary, .user-menu > summary:hover { background: var(--ld-blue-130); } +.user-menu .nav-pop { left: auto; right: 0; min-width: 128px; padding: 14px 8px; top: calc(100% - 8px); } +.user-menu .nav-pop a, .user-menu .nav-pop .linkish { font-size: 14px; padding: 8px 12px; } +.avatar { + width: 34px; height: 34px; border-radius: 999px; background: var(--ld-spark-100); + color: var(--ld-blue-160); display: grid; place-items: center; font-weight: 700; font-size: 14px; +} +.linkish { + background: none; border: 0; font: inherit; cursor: pointer; padding: 0; + color: var(--ld-blue-160); +} + +/* ------------------------------ buttons --------------------------------- */ +.btn { + display: inline-block; border: 0; border-radius: 999px; cursor: pointer; + font: inherit; font-weight: 700; padding: 12px 24px; text-decoration: none; + background: var(--ld-blue-100); color: #fff; +} +.btn:hover { background: var(--ld-blue-130); color: #fff; } +.btn-secondary { background: #fff; color: var(--ld-blue-160); border: 1px solid var(--ld-blue-160); } +.btn-secondary:hover { background: var(--ld-gray-200); color: var(--ld-blue-160); } +.btn-spark { background: var(--ld-spark-100); color: var(--ld-blue-160); } +.btn-spark:hover { background: #ffd45c; color: var(--ld-blue-160); } +.btn-sm { padding: 7px 18px; font-size: 14px; } +.btn-block { display: block; width: 100%; text-align: center; padding: 14px 24px; } + +/* ------------------------------ hero ------------------------------------ */ +.hero { background: var(--ld-blue-100); color: #fff; padding: 0; position: relative; } +.hero .inner { max-width: 932px; margin: 0 auto; padding: 84px 0 300px; text-align: left; } +.hero h1 { font-size: 92px; line-height: 1.18; font-weight: 300; margin: 0 0 56px; } +.hero h1 span { display: block; } +.hero-search { + display: flex; align-items: center; background: var(--ld-blue-130); + border-radius: 999px; padding: 22px 22px 22px 40px; margin: 0; +} +.hero-search input { + flex: 1; min-width: 0; border: 0; outline: none; font-size: 24px; font-family: inherit; + color: #fff; background: transparent; font-weight: 300; +} +.hero-search input::placeholder { color: #fff; } +.hero-search button { + border: 0; background: #fff; color: var(--ld-blue-100); border-radius: 999px; + width: 68px; height: 68px; display: grid; place-items: center; cursor: pointer; +} +.hero-strip { + position: absolute; left: 0; right: 0; bottom: -232px; + display: grid; grid-template-columns: 330px 1fr 330px; gap: 46px; align-items: end; +} +.hero-strip > img, .hero-strip-mid img { + display: block; width: 100%; height: 380px; object-fit: cover; border-radius: 20px; +} +.hero-strip > img:first-child { border-radius: 0 20px 20px 0; } +.hero-strip > img:last-child { border-radius: 20px 0 0 20px; } +.hero-strip-mid { position: relative; } +.hero-pill { + position: absolute; right: 62px; bottom: 76px; + background: var(--ld-blue-100); color: #fff; border-color: var(--ld-blue-100); + font-weight: 400; font-size: 15px; padding: 10px 24px; +} +.hero-pill:hover { background: var(--ld-blue-130); color: #fff; } +.after-hero { padding-top: 400px; } + +/* ------------------------------ sections -------------------------------- */ +section { padding: 56px 0; } +section h2 { font-size: 40px; font-weight: 300; margin: 0 0 32px; line-height: 1.15; } +section h2.tight { margin-bottom: 12px; } +section h3 { font-size: 22px; font-weight: 700; margin: 0 0 12px; } +.section-blurb { font-size: 15px; max-width: 680px; margin: 0 0 40px; } +.section-alt { background: var(--ld-gray-5); } +.section-blue { background: var(--ld-blue-160); color: #fff; } +.section-blue h2, .section-blue a { color: #fff; } + +/* career-area ribbon */ +.ribbon-section { padding: 90px 0 60px; } +.ribbon { display: flex; border-radius: 40px; overflow: hidden; } +.ribbon-seg { + flex: 1 1 0; display: flex; align-items: center; justify-content: space-between; gap: 12px; + padding: 44px 30px 44px 24px; text-decoration: none; font-size: 18px; font-weight: 300; + line-height: 1.3; border-radius: 40px 0 0 40px; margin-left: -40px; padding-left: 60px; +} +.ribbon-seg:first-child { margin-left: 0; padding-left: 24px; } +.ribbon-seg svg { flex: 0 0 auto; } +.seg-1 { background: var(--ld-blue-10); color: var(--ld-blue-160); z-index: 5; } +.seg-2 { background: var(--ld-sky); color: var(--ld-blue-160); z-index: 4; } +.seg-3 { background: var(--ld-blue-100); color: #fff; z-index: 3; } +.seg-4 { background: var(--ld-blue-130); color: #fff; z-index: 2; } +.seg-5 { background: var(--ld-blue-160); color: #fff; z-index: 1; } +.ribbon-seg:hover { filter: brightness(1.06); color: inherit; } + +/* bento */ +.bento-section { padding: 40px 0 70px; } +.bento-layout { display: grid; grid-template-columns: 1040px 1fr; gap: 24px; align-items: start; } +.bento { display: grid; grid-template-columns: repeat(6, 1fr); gap: 24px; } +.tile { + position: relative; border-radius: 24px; overflow: hidden; text-decoration: none; + display: block; color: var(--ld-blue-160); +} +.tile-sq { grid-column: span 2; height: 296px; } +.tile-wide { grid-column: span 4; height: 296px; } +.tile-short { grid-column: span 2; height: 150px; } +.tile-mini { grid-column: span 1; height: 150px; } +.tile-gap { grid-column: span 2; } +.tile-sky { background: var(--ld-sky); } +.tile-pale { background: var(--ld-blue-10); } +.tile-blue { background: var(--ld-blue-100); color: #fff; } +.tile-navy { background: var(--ld-blue-160); color: #fff; } +.tile-spark { background: var(--ld-spark-100); } +.tile-photo img { display: block; width: 100%; height: 100%; object-fit: cover; } +.tile-center { display: grid; place-items: center; } +.tile-label { position: absolute; left: 24px; top: 22px; right: 24px; font-size: 26px; font-weight: 300; line-height: 1.25; } +.spark-card { width: 130px; height: 130px; } +.tile-heart { width: 86px; height: 86px; } +.tile-diamond { width: 140px; height: 140px; filter: brightness(0) invert(1); } +.circle-arrow { + position: absolute; left: 24px; bottom: 22px; width: 48px; height: 48px; border-radius: 999px; + border: 1px solid currentColor; display: grid; place-items: center; font-size: 30px; line-height: 1; + font-weight: 300; +} +a.tile:hover { color: inherit; filter: brightness(1.04); } +a.tile.tile-sky:hover, a.tile.tile-pale:hover { color: var(--ld-blue-160); } +.bento-aside { padding-top: 12px; } +.bento-aside h2 { font-size: 40px; font-weight: 300; margin: 0 0 22px; } + +/* benefits */ +.benefits-layout { display: grid; grid-template-columns: 1fr 360px; gap: 60px; align-items: start; } +.benefit-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 34px 40px; } +.benefit-item img { width: 60px; height: 60px; display: block; margin-bottom: 10px; } +.benefit-item b { display: block; font-size: 13px; margin-bottom: 4px; } +.benefit-item span { font-size: 13px; color: var(--ld-blue-160); display: block; line-height: 1.45; } +.benefits-aside p { font-size: 30px; font-weight: 300; line-height: 1.35; margin: 0 0 30px; } +/* shared benefits partial (job detail + career-area pages) */ +.benefits-block { padding: 80px 0 0; } +.benefits-block h2 { font-size: 40px; font-weight: 300; margin: 0 0 32px; line-height: 1.15; } +.benefit-rows { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 28px 40px; margin-top: 30px; } +.benefit-row { display: flex; gap: 20px; align-items: flex-start; } +.benefit-row img { width: 90px; height: 90px; flex: 0 0 auto; } +.benefit-row b { display: block; font-size: 13px; margin: 4px 0 2px; } +.benefit-row span { font-size: 13px; color: var(--ld-blue-160); display: block; line-height: 1.45; } +.benefits-cta { margin: 36px 0 0; } + +/* milestones */ +.milestones { padding: 90px 0 60px; } +.milestone-layout { display: grid; grid-template-columns: 360px 1fr; gap: 20px; align-items: start; } +.milestone-layout h2 { font-size: 64px; font-weight: 300; line-height: 1.05; margin: 90px 0 0; } +.badge-stack { display: flex; flex-direction: column; gap: 110px; } +.badge-row { display: flex; align-items: flex-start; gap: 24px; } +.badge-row.row-2, .badge-row.row-4 { padding-left: 0; } +.badge-row.row-3 { padding-left: 0; } +.badge { + width: 452px; border-radius: 26px; overflow: hidden; border: 1px solid var(--ld-gray-20); + text-align: center; box-shadow: 0 1px 2px rgba(0, 30, 96, .08); background: #fff; +} +.badge-top { height: 48px; position: relative; } +.badge-top .grip { + position: absolute; left: 50%; top: 8px; transform: translateX(-50%); + width: 40px; height: 7px; border-radius: 999px; background: #fff; +} +.badge-top em { position: absolute; right: 50px; top: 18px; font-style: normal; font-size: 16px; } +.badge-body { padding: 40px 36px; font-size: 24px; line-height: 1.4; min-height: 160px; display: grid; place-items: center; } +.badge-foot { padding: 20px 0 26px; font-weight: 800; color: var(--ld-blue-100); font-size: 24px; letter-spacing: -.5px; } +.badge-foot.plain { font-weight: 400; color: var(--ld-blue-160); font-size: 16px; } +.badge-sky .badge-top { background: var(--ld-sky-60); } +.badge-sky .badge-body { background: var(--ld-blue-100); color: #fff; } +.badge-spark .badge-top { background: var(--ld-spark-100); color: var(--ld-blue-160); } +.badge-spark .badge-body { background: var(--ld-spark-30); } +.badge-navy .badge-top { background: var(--ld-blue-130); } +.badge-navy .badge-body { background: #fff; } +.badge-blue .badge-top { background: var(--ld-blue-100); color: #fff; } +.badge-blue .badge-body { background: var(--ld-blue-160); color: #fff; } +.badge-blank { width: 214px; height: 132px; border-radius: 26px; flex: 0 0 auto; } +.blank-sky { background: var(--ld-sky-60); } +.blank-spark { background: var(--ld-spark-30); } +.blank-navy { background: transparent; } +.blank-blue { background: var(--ld-blue-100); } + +/* video strip */ +.video-strip { display: grid; grid-template-columns: 300px 1fr 300px; gap: 24px; } +.video-tile { position: relative; height: 386px; border-radius: 24px; overflow: hidden; background: var(--ld-blue-100); color: #fff; } +.video-tile > img.v-photo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: .55; } +.video-strip.dark .video-tile { background: #000; } +.v-spark { position: absolute; left: 50%; top: 150px; transform: translateX(-50%); width: 76px; height: 76px; } +.v-cap { position: absolute; left: 24px; bottom: 26px; } +.v-cap em { display: block; font-style: normal; font-size: 14px; } +.v-cap b { display: block; font-size: 20px; margin: 4px 0 18px; } +.play { + display: block; width: 56px; height: 56px; border-radius: 999px; background: #fff; position: relative; +} +.play::after { + content: ""; position: absolute; left: 21px; top: 16px; border-left: 20px solid var(--ld-blue-100); + border-top: 12px solid transparent; border-bottom: 12px solid transparent; +} +.dots { display: flex; justify-content: center; gap: 12px; margin-top: 34px; } +.dots i { width: 12px; height: 12px; border-radius: 999px; border: 1.5px solid var(--ld-blue-160); display: block; } +.dots i.on { width: 100px; background: linear-gradient(90deg, var(--ld-blue-160) 24%, #fff 24%); } + +/* find the role */ +.find-role { padding: 120px 0 130px; } +.find-role h2 { font-size: 90px; font-weight: 300; margin: 0 0 56px; letter-spacing: -.5px; } +.find-search { + display: flex; align-items: center; background: var(--ld-gray-5); border: 1px solid var(--ld-gray-20); + border-radius: 999px; padding: 14px 14px 14px 40px; +} +.find-search input { + flex: 1; min-width: 0; border: 0; outline: none; font-size: 24px; font-family: inherit; + font-weight: 300; color: var(--ld-blue-160); background: transparent; +} +.find-search input::placeholder { color: var(--ld-text-subtle); } +.find-search button { + border: 0; background: var(--ld-blue-100); color: #fff; border-radius: 999px; + width: 68px; height: 68px; display: grid; place-items: center; cursor: pointer; +} + +/* quote band */ +.quote-section { background: var(--ld-blue-100); color: #fff; padding: 160px 0; } +.quote-band-text { max-width: 620px; margin: 0 auto; font-size: 30px; font-weight: 300; line-height: 1.35; } + +.quote-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; padding-top: 110px; } +.quote-card { + border-radius: 16px; background: #fff; box-shadow: 0 6px 24px rgba(0, 30, 96, .14); + padding: 24px 28px 30px; align-self: end; +} +.quote-card.on { transform: translateY(-40px); } +.quote-card b { display: block; font-size: 14px; margin-bottom: 10px; } +.quote-card p { margin: 0; font-size: 17px; line-height: 1.4; } + +.carousel { display: grid; grid-template-columns: repeat(5, 1fr); gap: 20px; } +.carousel a { + display: block; text-decoration: none; color: var(--ld-blue-160); + border-radius: 24px; overflow: hidden; background: var(--ld-blue-10); +} +.carousel img { display: block; width: 100%; height: 220px; object-fit: cover; } +.carousel .cap { padding: 16px 18px 20px; } +.carousel .cap b { display: block; font-size: 18px; } +.carousel .cap em { display: block; font-style: normal; color: var(--ld-text-subtle); font-size: 14px; } +.carousel .cap i { display: block; font-style: normal; margin-top: 8px; color: var(--ld-blue-100); font-weight: 600; font-size: 14px; } + +.stat-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; } +.stat-card { background: var(--ld-blue-10); border-radius: 24px; padding: 28px; } +.stat-card b { display: block; font-size: 34px; font-weight: 700; color: var(--ld-blue-100); } + +.tile-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; } +.tile-grid .tile { border-radius: 24px; overflow: hidden; background: var(--ld-gray-5); } +.tile-grid .tile img { display: block; width: 100%; height: 200px; object-fit: cover; } +.tile-grid .tile .cap { padding: 16px 18px; } + +/* ------------------------------ job cards ------------------------------- */ +.job-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 22px; } +.job-grid.one-col { grid-template-columns: 1fr; } +.job-grid.three-col { grid-template-columns: repeat(3, minmax(0, 1fr)); } +.job-card { + border: 1px solid var(--ld-gray-20); border-radius: 24px; padding: 22px 24px 24px; + background: #fff; display: block; position: relative; +} +.job-card:hover { border-color: var(--ld-blue-130); } +.job-card .spark { width: 24px; height: 24px; display: block; margin-bottom: 14px; } +.job-card h3 { margin: 0 0 10px; font-size: 17px; font-weight: 700; } +.job-card h3 a { color: var(--ld-blue-160); text-decoration: none; } +.job-card h3 a:hover { text-decoration: underline; } +.job-card .card-link::after { content: ""; position: absolute; inset: 0; border-radius: 24px; } +.job-card .meta { font-size: 15px; color: var(--ld-blue-160); } +.job-card .meta div { margin-bottom: 2px; } +.job-card .pay { margin-top: 6px; font-size: 15px; } +.job-card .actions { margin-top: 18px; display: flex; gap: 10px; align-items: center; position: relative; z-index: 1; } +.job-card .actions form { margin: 0; } +.job-card-compact { padding: 18px 22px 22px; } +.job-card-compact h3 { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + +/* the live "Select +" pill */ +.pill { + display: inline-flex; align-items: center; gap: 10px; border-radius: 999px; + border: 1px solid var(--ld-blue-160); background: #fff; color: var(--ld-blue-160); + font: inherit; font-size: 14px; font-weight: 700; padding: 8px 18px; + text-decoration: none; cursor: pointer; +} +.pill span { font-weight: 400; font-size: 16px; } +.pill:hover { background: var(--ld-blue-9); color: var(--ld-blue-160); } +.pill-on { background: var(--ld-blue-160); color: #fff; } +.pill-on:hover { background: var(--ld-blue-130); color: #fff; } + +/* ------------------------------ results --------------------------------- */ +.results-layout { display: grid; grid-template-columns: 372px 1fr; gap: 28px; padding: 28px 0 64px; } +.results-layout { max-width: none; } +.results-page { padding: 0 24px; } +.results-aside .map-panel { border-radius: 20px; overflow: hidden; position: sticky; top: 100px; } +.cluster-map { display: block; } +.pin-card { display: block; border-radius: 16px; } + +.tabs { display: flex; gap: 32px; border-bottom: 1px solid var(--ld-gray-20); margin: 0 0 22px; } +.tabs a { + text-decoration: none; color: var(--ld-text-subtle); padding: 10px 2px 14px; + border-bottom: 3px solid transparent; font-weight: 600; display: inline-flex; + align-items: center; gap: 8px; +} +.tabs a.active { color: var(--ld-blue-160); border-bottom-color: var(--ld-blue-100); } +.tab-badge { + background: var(--ld-blue-100); color: #fff; border-radius: 999px; + padding: 2px 10px; font-size: 12px; font-weight: 700; +} + +.results-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; } +.results-head h1 { font-size: 34px; font-weight: 300; margin: 0; } +.loc-note { color: var(--ld-text-subtle); font-size: 15px; margin: 10px 0 0; } + +/* popovers: Location, Filters and Sort by */ +.pop-wrap { position: relative; } +.pop-wrap > summary { list-style: none; cursor: pointer; } +.pop-wrap > summary::-webkit-details-marker { display: none; } +.pop-wrap > summary::marker { content: ""; } +.loc-link { + display: inline-flex; align-items: center; gap: 8px; color: var(--ld-blue-100); + font-size: 15px; padding: 8px 2px; +} +.loc-link:hover { text-decoration: underline; } +.tool-btn { + display: inline-flex; align-items: center; gap: 8px; color: var(--ld-blue-160); + font-size: 15px; padding: 8px 10px; border-radius: 8px; +} +.tool-btn:hover { background: var(--ld-gray-5); } +.tool-btn .caret { font-size: 12px; } +.toolbar { display: flex; justify-content: flex-end; align-items: center; gap: 18px; margin: 12px 0 16px; } +.active-filters { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin: 0 0 20px; } +.active-chip { + display: inline-flex; align-items: center; gap: 8px; border-radius: 999px; + border: 1px solid var(--ld-gray-20); background: var(--ld-blue-9); + color: var(--ld-blue-160); text-decoration: none; padding: 6px 14px; font-size: 14px; +} +.active-chip:hover { border-color: var(--ld-blue-130); color: var(--ld-blue-160); } +.clear-all { font-size: 14px; color: var(--ld-blue-160); text-decoration: underline; } + +.pop-panel { + position: absolute; right: 0; top: calc(100% + 8px); z-index: 40; background: #fff; + border-radius: 20px; box-shadow: 0 10px 34px rgba(0, 30, 96, .22); padding: 22px 24px; + min-width: 340px; text-align: left; +} +.pop-panel.wide { min-width: 700px; } +.pop-head h2 { font-size: 20px; font-weight: 400; margin: 0 0 16px; } +.pop-panel .field { margin-bottom: 14px; } +.pop-panel .radio-row { + display: flex; align-items: center; gap: 10px; padding: 7px 0; font-size: 15px; + color: var(--ld-blue-160); text-decoration: none; cursor: pointer; +} +.pop-panel .radio-row:hover { color: var(--ld-blue-100); } +.pop-actions { + display: flex; justify-content: flex-end; align-items: center; gap: 18px; + border-top: 1px solid var(--ld-gray-20); margin-top: 16px; padding-top: 16px; +} +.pop-actions .reset { color: var(--ld-blue-160); text-decoration: underline; font-size: 15px; } + +.filter-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 22px; } +.filter-grid fieldset { border: 0; padding: 0; margin: 0; min-width: 0; } +.filter-grid legend { font-weight: 700; padding: 0 0 8px; font-size: 16px; } +.filter-grid label { display: flex; gap: 8px; align-items: flex-start; font-size: 14px; padding: 4px 0; cursor: pointer; } +.filter-grid .cat-group { margin: 2px 0 10px 20px; } +.filter-grid .cat-group summary { cursor: pointer; font-size: 13px; padding: 3px 0; color: var(--ld-text-subtle); } +.area-col { max-height: 420px; overflow-y: auto; } + +.empty-panel { background: var(--ld-blue-9); border-radius: 24px; padding: 32px; } + +.pagination { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 32px; align-items: center; justify-content: center; } +.pagination a, .pagination span { + min-width: 40px; height: 40px; border-radius: 999px; display: grid; place-items: center; + text-decoration: none; border: 1px solid transparent; color: var(--ld-blue-160); padding: 0 12px; +} +.pagination a:hover { background: var(--ld-gray-5); } +.pagination .current { border-color: var(--ld-blue-100); color: var(--ld-blue-100); font-weight: 700; } + +/* ------------------------------ job detail ------------------------------ */ +.detail-top { padding: 22px 0 0; } +.hero-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 24px; align-items: start; } +.hero-col { display: flex; flex-direction: column; gap: 24px; } +.spark-pill { + background: var(--ld-blue-100); border-radius: 999px; height: 74px; + display: grid; place-items: center; +} +.spark-pill img { height: 34px; width: auto; } +.hero-photo-tall { display: block; width: 100%; height: 424px; object-fit: cover; border-radius: 20px; } +.hero-photo-wide { display: block; width: 100%; height: 298px; object-fit: cover; border-radius: 20px; } + +.id-card { border-radius: 20px; overflow: hidden; color: #fff; } +.id-card .id-top { background: var(--ld-blue-100); padding: 14px 0 10px; text-align: center; } +.id-card .grip { + display: block; width: 42px; height: 5px; border-radius: 999px; + background: rgba(255, 255, 255, .9); margin: 0 auto 14px; +} +.id-card .id-spark { height: 30px; width: auto; } +.id-card .id-body { padding: 12px 20px 18px; text-align: center; } +.id-card .id-title { font-size: 18px; } +.id-card .id-loc { font-size: 14px; font-weight: 700; margin-top: 8px; } +.id-salaried .id-body { background: var(--ld-blue-100); } +.id-hourly .id-body { background: var(--ld-blue-160); } +.id-card .id-actions { + background: #fff; display: flex; align-items: center; gap: 8px; padding: 12px 16px; + border: 1px solid var(--ld-gray-20); border-top: 0; + border-radius: 0 0 20px 20px; +} +.id-card .id-actions form { margin: 0; } +.icon-btn { + background: none; border: 0; cursor: pointer; color: var(--ld-blue-160); + width: 36px; height: 36px; border-radius: 999px; display: grid; place-items: center; padding: 0; +} +.icon-btn:hover { background: var(--ld-gray-200); color: var(--ld-blue-160); } +.apply-now { margin-left: auto; display: inline-flex; align-items: center; gap: 8px; } +.apply-now span { font-weight: 400; } + +.detail-layout { + display: grid; grid-template-columns: 236px minmax(0, 1000px); gap: 104px; + padding: 40px 100px 0 0; max-width: 1440px; margin: 0 auto; +} +.detail-nav { + position: sticky; top: 100px; align-self: start; background: var(--ld-blue-9); + border-radius: 0 24px 24px 0; padding: 26px 20px 26px 24px; +} +.detail-nav ul { list-style: none; margin: 0; padding: 0; } +.detail-nav li { padding: 7px 0; font-size: 14px; } +.detail-nav a { text-decoration: none; color: var(--ld-blue-160); } +.detail-nav li:first-child a { font-weight: 700; } +.detail-nav .sub { padding-left: 16px; font-size: 13px; } + +.detail-main h1 { font-size: 44px; font-weight: 300; margin: 0 0 30px; line-height: 1.1; } +.fact-row { display: grid; grid-template-columns: 1fr 490px; gap: 32px; align-items: start; } +.fact-col .banner-line { font-weight: 700; font-size: 20px; margin-bottom: 4px; } +.fact-col address { font-style: normal; line-height: 1.55; margin: 0 0 16px; } +.positions { + display: inline-block; background: var(--ld-blue-9); border-radius: 6px; + padding: 4px 10px; font-size: 13px; color: var(--ld-blue-160); +} +.fact-col .req-id { display: inline-block; font-size: 14px; color: var(--ld-blue-100); margin-top: 12px; text-decoration: none; } +.fact-col .req-id:hover { text-decoration: underline; } +.map-col { border-radius: 16px; overflow: hidden; } + +.chips { display: flex; flex-wrap: wrap; gap: 24px; margin: 28px 0 8px; } +.chip { + background: var(--ld-blue-160); color: #fff; border-radius: 24px; + padding: 20px 26px 18px; font-size: 15px; display: inline-flex; flex-direction: column; + align-items: flex-start; gap: 12px; min-height: 100px; min-width: 232px; justify-content: center; +} +.chip svg { color: #fff; width: 28px; height: 28px; } +.footnote { font-size: 13px; color: var(--ld-blue-160); margin-top: 14px; } + +.detail-body { margin-top: 36px; } +.detail-body h2 { font-size: 30px; font-weight: 300; margin: 36px 0 14px; } +.detail-body h2:first-child { margin-top: 0; } +.detail-body p { margin: 0 0 14px; } +.detail-body ul { margin: 0 0 16px; padding-left: 20px; } +.detail-body li { margin-bottom: 8px; } +a.hashtag { font-weight: 400; color: var(--ld-blue-100); text-decoration: none; } +a.hashtag:hover { text-decoration: underline; } +.detail-body .sub-heading { font-weight: 700; margin: 22px 0 6px; } +/* Workday-style salaried postings: bold run-in headings instead of the hourly h2s */ +.detail-body.salaried h2 { font-size: 15px; font-weight: 700; margin: 18px 0 4px; line-height: 1.4; } +.detail-body.salaried .preamble { font-style: italic; } +.legal { font-size: 13px; color: var(--ld-text-subtle); margin-top: 20px; } + +/* related roles under the detail layout */ +.related-section { padding: 110px 0 90px; } +.related-section .wrap-hub { max-width: 1440px; margin: 0 auto; padding: 0 120px; } +.related-section h2 { font-size: 22px; font-weight: 400; margin: 0 0 20px; } + +.benefit-tiles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; } +.benefit-tile { background: var(--ld-blue-160); color: #fff; border-radius: 24px; padding: 26px; } +.benefit-tile img { width: 44px; height: 44px; margin-bottom: 12px; } +.benefit-tile b { display: block; font-size: 20px; } +.benefit-tile em { font-style: normal; display: block; color: var(--ld-spark-100); margin-bottom: 10px; } +.benefit-tile p { font-size: 13px; margin: 8px 0 0; line-height: 1.45; } +.benefit-tile.tile-1 { background: var(--ld-blue-100); } +.benefit-tile.tile-1 em { color: #fff; font-weight: 700; } +.benefit-tile.tile-2 { background: var(--ld-sky); color: var(--ld-blue-160); } +.benefit-tile.tile-2 em { color: var(--ld-blue-160); font-weight: 700; } +.benefit-tile.tile-2 img { filter: brightness(0) saturate(100%) invert(9%) sepia(60%) saturate(4000%) hue-rotate(215deg); } +.benefit-tile.tile-3 em { color: #fff; font-weight: 700; } + +/* Life at Walmart (shared partial) */ +.life-block { padding: 90px 0 0; } +.life-block h2 { font-size: 40px; font-weight: 300; margin: 0 0 30px; line-height: 1.15; } +.life-lead { font-size: 24px; line-height: 1.35; margin: 0 0 44px; max-width: 960px; } +.life-grid { display: grid; grid-template-columns: minmax(0, 1fr) 486px; gap: 34px; align-items: start; } +.life-grid p { font-size: 14px; margin: 0 0 14px; } +.life-media img { display: block; width: 100%; height: 328px; object-fit: cover; border-radius: 20px; margin-bottom: 24px; } +.life-band { + background: var(--ld-blue-100); color: #fff; margin: 40px -100px 40px -100px; padding: 100px 120px; +} +.life-band p { max-width: 600px; margin: 0 auto; font-size: 30px; font-weight: 300; line-height: 1.35; } +.life-closing { font-size: 20px; font-weight: 300; color: #3f5a8f; margin: 0 0 14px; line-height: 1.4; } +.life-note { font-size: 15px; margin: 0; } +.about-life .life-band { margin-left: 0; margin-right: 0; border-radius: 24px; } + +/* ------------------------------ forms ----------------------------------- */ +.form-card { max-width: 520px; margin: 48px auto; border: 1px solid var(--ld-gray-20); border-radius: 24px; padding: 32px; } +.form-wide { max-width: 720px; } +.form-card h1 { font-size: 28px; font-weight: 400; margin: 0 0 20px; } +.field { margin-bottom: 16px; } +.field label { display: block; font-weight: 600; margin-bottom: 6px; font-size: 15px; } +.field input[type=text], .field input[type=email], .field input[type=password], .field input[type=tel], .field select { + width: 100%; padding: 12px 14px; border: 1px solid var(--ld-gray-20); border-radius: 12px; + font: inherit; background: var(--ld-gray-5); color: var(--ld-blue-160); +} +.field .check { display: flex; gap: 10px; align-items: flex-start; font-weight: 400; } +.errors { background: #fdecec; border: 1px solid var(--ld-red); color: var(--ld-red); border-radius: 12px; padding: 14px 18px; margin-bottom: 18px; } +.errors ul { margin: 0; padding-left: 18px; } +.flash { border-radius: 12px; padding: 12px 18px; margin: 16px 0; } +.flash.success { background: #eaf6e6; border: 1px solid var(--ld-green); color: #1c5c02; } +.flash.info { background: var(--ld-blue-9); border: 1px solid var(--ld-blue-100); } +.flash.warning { background: #fff5e0; border: 1px solid var(--ld-spark-100); color: #7a5a00; } +.form-note { font-size: 14px; color: var(--ld-text-subtle); margin-top: 14px; } + +/* stripped sign-in / register layout (identity.walmart.com) */ +.auth-body { background: #fff; min-height: 100vh; display: flex; flex-direction: column; } +.auth-main { flex: 1 0 auto; max-width: 352px; margin: 0 auto; padding: 28px 0 60px; width: 100%; } +.auth-spark { display: block; width: 56px; margin: 0 auto 20px; } +.auth-spark img { display: block; width: 56px; height: 56px; } +.auth-card h1 { font-size: 20px; font-weight: 700; text-align: center; margin: 0 0 22px; } +.auth-lead { text-align: center; font-size: 17px; margin: 0 0 26px; line-height: 1.4; } +.auth-card .field label { font-size: 14px; font-weight: 700; } +.auth-card .field input { + background: #fff; border: 1px solid var(--ld-blue-160); border-radius: 8px; padding: 16px 14px; +} +.auth-card .form-note { text-align: center; } +.auth-flash { margin: 0 0 18px; } +.auth-footer { + border-top: 1px solid var(--ld-gray-20); padding: 24px 80px 40px; display: flex; gap: 24px; + align-items: center; justify-content: space-between; font-size: 13px; color: var(--ld-blue-160); +} +.auth-footer nav { display: flex; gap: 24px; flex-wrap: wrap; } +.auth-footer a { color: var(--ld-blue-160); text-decoration: none; font-size: 14px; } +.auth-footer a:hover { text-decoration: underline; } + +.apply-page { background: #f0f8fa; padding-bottom: 140px; } +.apply-band { background: var(--ld-blue-100); height: 140px; } +.apply-card { + background: #fff; border-radius: 24px; max-width: 920px; margin: -80px auto 0; + padding: 60px 120px 48px; box-shadow: 0 1px 3px rgba(0, 30, 96, .08); +} +.apply-card h1 { font-size: 26px; font-weight: 300; margin: 0 0 22px; } +.apply-sub { font-size: 14px; margin: 0 0 24px; } +.apply-card .field label { font-size: 13px; font-weight: 700; } +.apply-card .field input[type=text], .apply-card .field input[type=email], .apply-card .field input[type=tel] { + background: #fff; border: 1px solid var(--ld-blue-160); border-radius: 4px; padding: 11px 12px; +} +.apply-hint { font-size: 11px; color: var(--ld-gray-100); margin: 6px 0 0; } +.field-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } +.apply-card .check { font-size: 13px; margin-top: 8px; } +.apply-card .check input { width: 18px; height: 18px; margin: 0; } +.apply-actions { display: flex; justify-content: flex-end; align-items: center; gap: 28px; margin-top: 32px; } +.apply-actions-start { justify-content: flex-start; gap: 12px; } +.apply-back { font-size: 14px; color: var(--ld-blue-160); } +.review-list { list-style: none; margin: 0 0 22px; padding: 0; } +.review-list li { display: flex; justify-content: space-between; gap: 20px; padding: 10px 0; border-bottom: 1px solid var(--ld-gray-20); } +.review-list b { font-weight: 600; } +.confirmation { + background: var(--ld-blue-10); border-radius: 24px; padding: 28px; margin: 24px 0; + font-size: 22px; font-weight: 700; +} + +table.data { width: 100%; border-collapse: collapse; } +table.data th, table.data td { text-align: left; padding: 12px 10px; border-bottom: 1px solid var(--ld-gray-20); } +table.data th { font-size: 14px; text-transform: uppercase; letter-spacing: .04em; color: var(--ld-text-subtle); } + +/* ------------------------------ area / locations ------------------------ */ +.area-hero { position: relative; } +.area-hero img { width: 100%; height: 780px; object-fit: cover; display: block; } +.area-card { + position: absolute; left: var(--gutter); top: 250px; width: 700px; background: var(--ld-blue-10); + border-radius: 24px; padding: 40px 30px 30px 16px; color: var(--ld-blue-160); +} +.area-card h1 { font-size: 34px; font-weight: 300; margin: 0 0 24px; } +.area-card p { font-size: 18px; line-height: 1.45; margin: 0 0 24px; } + +.join-grid { display: grid; grid-template-columns: 696px 1fr; gap: 116px; align-items: start; } +.stack-card { position: relative; padding-top: 40px; } +.stack-band { display: block; height: 30px; border-radius: 24px 24px 0 0; } +.band-navy { background: var(--ld-blue-130); position: absolute; top: 0; left: 0; right: 0; height: 60px; } +.band-sky { background: var(--ld-blue-10); position: absolute; top: 36px; left: 0; right: 0; height: 60px; } +.stack-card img { display: block; width: 100%; height: 450px; object-fit: cover; border-radius: 24px; position: relative; margin-top: 36px; } +.stack-controls { + position: absolute; left: 0; right: 0; bottom: 60px; display: flex; justify-content: center; gap: 22px; +} +.stack-controls i { width: 22px; height: 22px; border-radius: 999px; border: 1px solid #fff; color: #fff; font-style: normal; font-size: 12px; display: grid; place-items: center; } +.category-list { list-style: none; margin: 0; padding: 0; } +.category-list li { margin: 0 0 26px; } +.category-list a { font-size: 24px; font-weight: 300; text-decoration: none; color: var(--ld-blue-160); line-height: 1.3; } +.category-list a:hover { text-decoration: underline; color: var(--ld-blue-100); } +.category-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; } + +.hub-links { display: flex; justify-content: space-between; gap: 24px; padding: 60px 0 0; } +.hub-links a { + display: inline-flex; align-items: center; gap: 16px; text-decoration: none; color: var(--ld-blue-160); + font-size: 18px; font-weight: 300; +} +.hub-links a:hover { color: var(--ld-blue-100); } + +.hub-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 24px; } +.hub-card { border: 1px solid var(--ld-gray-20); border-radius: 24px; overflow: hidden; } +.hub-card img { display: block; width: 100%; height: 220px; object-fit: cover; } +.hub-card .cap { padding: 22px; } + +.loc-hero { position: relative; border-radius: 24px; overflow: hidden; margin-top: 0; } +.loc-hero img { display: block; width: 100%; height: 480px; object-fit: cover; } +.loc-hero h1 { + position: absolute; left: 60px; bottom: 60px; margin: 0; color: #fff; font-size: 72px; font-weight: 300; + text-shadow: 0 2px 12px rgba(0, 0, 0, .35); +} +.loc-hero .pause { + position: absolute; right: 40px; top: 40px; width: 48px; height: 48px; border-radius: 999px; + border: 1px solid #fff; color: #fff; display: grid; place-items: center; font-size: 16px; +} +.loc-intro { padding: 54px 0 20px; } +.loc-intro p { font-size: 22px; font-weight: 300; max-width: 720px; margin: 0; line-height: 1.35; } +.hub-tiles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 34px 28px; } +.hub-tile img { display: block; width: 100%; height: 202px; object-fit: cover; border-radius: 16px; margin-bottom: 14px; } +.hub-tile b { display: block; font-size: 14px; margin-bottom: 8px; } +.hub-tile p { font-size: 13px; margin: 0 0 24px; line-height: 1.45; } +.arrow-link { display: inline-flex; color: var(--ld-blue-160); } +.arrow-link:hover { color: var(--ld-blue-100); } + +/* saved roles promo */ +.promo-section { padding: 80px 0 60px; } +.promo { display: grid; grid-template-columns: 1fr 565px; gap: 40px; align-items: start; } +.promo h1 { font-size: 64px; font-weight: 300; line-height: 1.12; margin: 0 0 20px; } +.promo p { font-size: 17px; max-width: 560px; margin: 0 0 32px; } +.promo-photo { display: block; width: 565px; height: 376px; object-fit: cover; border-radius: 24px; } +.saved-section { padding: 40px 0 20px; } +.saved-heading { font-size: 48px; font-weight: 300; margin: 0 0 28px; } +.saved-note { font-size: 18px; max-width: 420px; margin: 0 0 40px; line-height: 1.4; } +.trending-section { padding: 20px 0 60px; } +.trending-heading { font-size: 22px; font-weight: 400; margin: 0 0 20px; } + +.faq-section { padding: 40px 0; } +.faq-layout { display: grid; grid-template-columns: 340px minmax(0, 1fr); gap: 150px; align-items: start; } +.faq-intro img { width: 150px; height: 150px; display: block; margin-bottom: 10px; } +.faq-intro h2 { font-size: 40px; font-weight: 300; margin: 0 0 14px; } +.faq-intro p { font-size: 14px; margin: 0; } +.faq details { + border: 1px solid var(--ld-gray-20); border-radius: 16px; padding: 0; margin-bottom: 18px; + background: #fff; +} +.faq summary { + cursor: pointer; font-weight: 700; font-size: 14px; padding: 22px 60px 22px 24px; list-style: none; + position: relative; +} +.faq summary::-webkit-details-marker { display: none; } +.faq summary::after { + content: ""; position: absolute; right: 26px; top: 22px; width: 10px; height: 10px; + border-right: 2px solid var(--ld-blue-160); border-bottom: 2px solid var(--ld-blue-160); + transform: rotate(45deg); +} +.faq details[open] summary::after { transform: rotate(225deg); top: 28px; } +.faq p { margin: 0; padding: 0 24px 22px; font-size: 14px; } + +.feature-section { padding: 90px 0 40px; } +.feature-row { display: grid; grid-template-columns: 690px minmax(0, 1fr); gap: 24px; align-items: center; margin-bottom: 70px; } +.feature-row img { display: block; width: 100%; height: 460px; object-fit: cover; border-radius: 24px; } +.feature-text h3 { font-size: 30px; font-weight: 400; margin: 0 0 14px; line-height: 1.25; } +.feature-text p { font-size: 14px; margin: 0 0 20px; } +.program-tiles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; margin-top: 30px; } +.program-tile { display: block; text-decoration: none; color: var(--ld-blue-160); } +.program-tile img { display: block; width: 100%; height: 320px; object-fit: cover; border-radius: 24px; margin-bottom: 18px; } +.program-tile b { display: block; font-size: 14px; margin-bottom: 8px; } +.program-tile p { font-size: 13px; margin: 0 0 22px; line-height: 1.45; } +.program-tile:hover { color: var(--ld-blue-100); } + +.meet-section { padding: 30px 0 60px; } +.meet-layout { display: grid; grid-template-columns: 760px minmax(0, 1fr); gap: 24px; align-items: start; } +.meet-tile { height: 430px; } +.meet-tile .v-spark { width: 110px; height: 110px; top: 130px; } +.meet-text h2 { font-size: 40px; font-weight: 300; margin: 20px 0 30px; } +.meet-text p { font-size: 14px; margin: 0; } +.values-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; } +.seg-6 { background: var(--ld-sky-60); color: var(--ld-blue-160); z-index: 0; } +.steps { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; } +.steps .step { background: var(--ld-blue-10); border-radius: 24px; padding: 24px; } + +/* ------------------------------ footer ---------------------------------- */ +.site-footer { background: var(--ld-blue-160); color: #fff; padding: 80px 0 60px; margin-top: 0; } +.site-footer .wrap { padding: 0 9.2%; max-width: none; } +.site-footer a { color: #fff; text-decoration: none; } +.site-footer a:hover { text-decoration: underline; color: #fff; } +.footer-cols { display: grid; grid-template-columns: repeat(4, 240px); gap: 0; margin-bottom: 44px; } +.footer-cols h4 { font-size: 24px; font-weight: 400; margin: 0 0 22px; } +.footer-cols ul { list-style: none; margin: 0; padding: 0; } +.footer-cols li { margin-bottom: 22px; font-size: 14px; padding-right: 24px; } +/* the harvested glyphs are dark (#151F29): the live footer draws them on white discs */ +.social { display: flex; gap: 12px; margin-bottom: 44px; } +.social-btn { + width: 44px; height: 44px; border-radius: 999px; background: #fff; + display: grid; place-items: center; flex: 0 0 auto; +} +.social-btn img { width: 24px; height: 25px; display: block; } +.legal-text { font-size: 11px; line-height: 2.3; color: #fff; margin: 0 0 22px; max-width: 1180px; } +.legal-link { font-weight: 700; text-decoration: underline !important; } +.footer-bottom { display: flex; gap: 12px; flex-wrap: wrap; font-size: 12px; padding-top: 8px; align-items: center; } +.footer-bottom a { text-decoration: underline; } + +/* Header: below 1024px the five nav items plus the search pill no longer fit on one + row (the header used to force the viewport wider than 768px), so the header wraps + and the search pill takes a full second row. */ +@media (max-width: 1024px) { + .site-header .wrap { flex-wrap: wrap; gap: 8px 12px; padding: 8px 16px; } + .main-nav { flex: 1 1 auto; gap: 2px; min-width: 0; flex-wrap: wrap; overflow: visible; } + .main-nav .nav-link, .nav-menu > summary { font-size: 14px; padding: 8px 8px; } + .header-search { flex: 1 0 100%; order: 10; } + .header-search form { max-width: none; } + .header-spacer { display: none; } +} + +@media (max-width: 1200px) { + :root { --gutter: 32px; } + .results-layout, .detail-layout { grid-template-columns: 1fr; } + .detail-layout { padding: 32px 24px 0; gap: 32px; } + .detail-nav { border-radius: 24px; } + .life-grid, .faq-layout, .feature-row, .meet-layout, .program-tiles, .benefit-rows, .field-pair, .values-grid { grid-template-columns: 1fr; } + .life-band { margin-left: 0; margin-right: 0; border-radius: 24px; padding: 60px 32px; } + .footer-cols { grid-template-columns: repeat(2, 1fr); gap: 24px; } + .apply-card { margin-left: 24px; margin-right: 24px; padding: 40px 32px; } + .related-section .wrap-hub { padding: 0 24px; } + .faq-layout { gap: 40px; } + .feature-row img, .meet-tile { height: 320px; } + .carousel, .stat-grid, .tile-grid, .steps, .footer-cols { grid-template-columns: repeat(2, 1fr); } + .job-grid, .job-grid.three-col, .category-grid, .benefit-tiles, .hub-grid, .hub-tiles, + .promo, .join-grid, .bento-layout, .benefits-layout, .milestone-layout { grid-template-columns: 1fr; } + .bento { grid-template-columns: repeat(2, 1fr); } + .tile-sq, .tile-wide, .tile-short, .tile-mini, .tile-gap { grid-column: span 1; } + .hero-strip { position: static; grid-template-columns: 1fr; padding: 0; } + .hero .inner { padding: 56px 32px; } + .after-hero { padding-top: 56px; } + .hero-grid { grid-template-columns: 1fr; } + .fact-row, .chips { grid-template-columns: 1fr; } + .filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .pop-panel, .pop-panel.wide { min-width: 300px; } + .detail-nav, .fact-card, .results-aside .map-panel { position: static; } + .hero h1, .find-role h2 { font-size: 40px; } + .area-card { position: static; width: auto; margin: -80px 32px 0; } + .area-hero img { height: 480px; } + .video-strip { grid-template-columns: 1fr; } + .badge, .promo-photo { width: 100%; } + .milestone-layout h2 { margin-top: 0; } + .ribbon { flex-direction: column; } + .ribbon-seg, .ribbon-seg:first-child { margin-left: 0; padding-left: 24px; border-radius: 0; } +} + +@media (max-width: 600px) { + :root { --gutter: 16px; } + html, body { max-width: 100%; overflow-x: clip; } + .wrap, .wrap-narrow, .wrap-bleed, .l1 .wrap { padding-left: 16px; padding-right: 16px; } + .site-header .wrap { padding: 8px 12px; } + .site-footer .wrap { padding-left: 16px; padding-right: 16px; } + .social { flex-wrap: wrap; } + .results-page { padding-left: 12px; padding-right: 12px; } + .main-nav { flex-wrap: wrap; overflow: visible; order: 8; flex-basis: 100%; } + .main-nav .nav-link, .nav-menu > summary { padding: 6px; font-size: 13px; } + .nav-pop, .user-menu .nav-pop { + position: fixed; inset: 190px 12px auto; width: auto; min-width: 0; + max-height: calc(100vh - 202px); overflow: auto; + } + .header-search { flex-basis: 100%; } + .header-search form { padding-left: 16px; } + .milestones { padding-top: 56px; } + .badge-stack { gap: 28px; min-width: 0; } + .badge-row { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; min-width: 0; } + .badge-blank { display: none; } + .badge, .badge-body { width: 100%; min-width: 0; } + .badge-body { padding: 28px 20px; font-size: 19px; } + .quote-cards { grid-template-columns: 1fr; padding-top: 48px; } + .quote-card.on { transform: none; } + .hub-links { flex-wrap: wrap; justify-content: flex-start; padding-top: 32px; } + .hub-links a { max-width: 100%; overflow-wrap: anywhere; } + .area-card { margin: -48px 16px 0; padding: 28px 20px; } + .area-card h1, .benefits-block h2 { font-size: 30px; overflow-wrap: anywhere; } + .area-hero img { height: 320px; } + .pop-panel, .pop-panel.wide { + position: fixed; inset: 72px 12px auto; width: auto; min-width: 0; max-height: calc(100vh - 84px); + overflow: auto; padding: 18px; z-index: 200; + } + .filter-grid, .benefit-grid, .steps, .job-grid, .job-grid.three-col { grid-template-columns: minmax(0, 1fr); } + .job-card, .job-card-compact, .steps .step { min-width: 0; } + .loc-hero h1 { left: 20px; right: 20px; bottom: 28px; width: auto; font-size: 40px; overflow-wrap: anywhere; } + .tabs, .toolbar, .results-head { flex-wrap: wrap; } + .apply-card, .detail-layout { margin-left: 0; margin-right: 0; padding-left: 16px; padding-right: 16px; } + .find-role { padding: 64px 0; } + .find-role h2, .hero h1 { font-size: 34px; overflow-wrap: anywhere; } + .auth-main { width: auto; margin-left: 16px; margin-right: 16px; } + .auth-footer { padding: 24px 16px 32px; } + .table-scroll { width: 100%; overflow-x: auto; } + table.data { min-width: 600px; } +} + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } +} diff --git a/sites/walmart_careers/static/fonts/EverydaySansUI-wght.ttf b/sites/walmart_careers/static/fonts/EverydaySansUI-wght.ttf new file mode 100644 index 00000000..7b258008 Binary files /dev/null and b/sites/walmart_careers/static/fonts/EverydaySansUI-wght.ttf differ diff --git a/sites/walmart_careers/static/icons/.gitkeep b/sites/walmart_careers/static/icons/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/sites/walmart_careers/static/icons/benefit-financial.svg b/sites/walmart_careers/static/icons/benefit-financial.svg new file mode 100644 index 00000000..ca73078f --- /dev/null +++ b/sites/walmart_careers/static/icons/benefit-financial.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sites/walmart_careers/static/icons/benefit-growth.svg b/sites/walmart_careers/static/icons/benefit-growth.svg new file mode 100644 index 00000000..69f7fdc2 --- /dev/null +++ b/sites/walmart_careers/static/icons/benefit-growth.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/sites/walmart_careers/static/icons/benefit-health.svg b/sites/walmart_careers/static/icons/benefit-health.svg new file mode 100644 index 00000000..ccccbd9a --- /dev/null +++ b/sites/walmart_careers/static/icons/benefit-health.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sites/walmart_careers/static/icons/benefit-pto.svg b/sites/walmart_careers/static/icons/benefit-pto.svg new file mode 100644 index 00000000..5df7085e --- /dev/null +++ b/sites/walmart_careers/static/icons/benefit-pto.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/sites/walmart_careers/static/icons/benefit-wellbeing.svg b/sites/walmart_careers/static/icons/benefit-wellbeing.svg new file mode 100644 index 00000000..f4982f36 --- /dev/null +++ b/sites/walmart_careers/static/icons/benefit-wellbeing.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sites/walmart_careers/static/icons/header-mobile-logo.svg b/sites/walmart_careers/static/icons/header-mobile-logo.svg new file mode 100644 index 00000000..47f18701 --- /dev/null +++ b/sites/walmart_careers/static/icons/header-mobile-logo.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/sites/walmart_careers/static/icons/heart-blue.svg b/sites/walmart_careers/static/icons/heart-blue.svg new file mode 100644 index 00000000..f15b2474 --- /dev/null +++ b/sites/walmart_careers/static/icons/heart-blue.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/sites/walmart_careers/static/icons/home-logo.svg b/sites/walmart_careers/static/icons/home-logo.svg new file mode 100644 index 00000000..a080f4fc --- /dev/null +++ b/sites/walmart_careers/static/icons/home-logo.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/sites/walmart_careers/static/icons/sams-club-text.svg b/sites/walmart_careers/static/icons/sams-club-text.svg new file mode 100644 index 00000000..a96751ae --- /dev/null +++ b/sites/walmart_careers/static/icons/sams-club-text.svg @@ -0,0 +1,4 @@ + + + diff --git a/sites/walmart_careers/static/icons/sams-logo.svg b/sites/walmart_careers/static/icons/sams-logo.svg new file mode 100644 index 00000000..a36fca73 --- /dev/null +++ b/sites/walmart_careers/static/icons/sams-logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/sites/walmart_careers/static/icons/sams-spark.svg b/sites/walmart_careers/static/icons/sams-spark.svg new file mode 100644 index 00000000..0f3656c0 --- /dev/null +++ b/sites/walmart_careers/static/icons/sams-spark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/sites/walmart_careers/static/icons/search_icon.png b/sites/walmart_careers/static/icons/search_icon.png new file mode 100644 index 00000000..b8cc7802 Binary files /dev/null and b/sites/walmart_careers/static/icons/search_icon.png differ diff --git a/sites/walmart_careers/static/icons/social-facebook.svg b/sites/walmart_careers/static/icons/social-facebook.svg new file mode 100644 index 00000000..406cd57c --- /dev/null +++ b/sites/walmart_careers/static/icons/social-facebook.svg @@ -0,0 +1,3 @@ + + + diff --git a/sites/walmart_careers/static/icons/social-glassdoor.svg b/sites/walmart_careers/static/icons/social-glassdoor.svg new file mode 100644 index 00000000..f16bd4c7 --- /dev/null +++ b/sites/walmart_careers/static/icons/social-glassdoor.svg @@ -0,0 +1,3 @@ + + + diff --git a/sites/walmart_careers/static/icons/social-instagram.svg b/sites/walmart_careers/static/icons/social-instagram.svg new file mode 100644 index 00000000..0f11f5c9 --- /dev/null +++ b/sites/walmart_careers/static/icons/social-instagram.svg @@ -0,0 +1,4 @@ + + + + diff --git a/sites/walmart_careers/static/icons/social-linkedin.svg b/sites/walmart_careers/static/icons/social-linkedin.svg new file mode 100644 index 00000000..be19b763 --- /dev/null +++ b/sites/walmart_careers/static/icons/social-linkedin.svg @@ -0,0 +1,3 @@ + + + diff --git a/sites/walmart_careers/static/icons/social-x.svg b/sites/walmart_careers/static/icons/social-x.svg new file mode 100644 index 00000000..7ab8ac69 --- /dev/null +++ b/sites/walmart_careers/static/icons/social-x.svg @@ -0,0 +1,3 @@ + + + diff --git a/sites/walmart_careers/static/icons/social-youtube.svg b/sites/walmart_careers/static/icons/social-youtube.svg new file mode 100644 index 00000000..cffa63ed --- /dev/null +++ b/sites/walmart_careers/static/icons/social-youtube.svg @@ -0,0 +1,3 @@ + + + diff --git a/sites/walmart_careers/static/icons/spark-white.svg b/sites/walmart_careers/static/icons/spark-white.svg new file mode 100644 index 00000000..a757601d --- /dev/null +++ b/sites/walmart_careers/static/icons/spark-white.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/sites/walmart_careers/static/icons/spark-yellow-card.svg b/sites/walmart_careers/static/icons/spark-yellow-card.svg new file mode 100644 index 00000000..4ac91fd6 --- /dev/null +++ b/sites/walmart_careers/static/icons/spark-yellow-card.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/sites/walmart_careers/static/icons/spark-yellow.svg b/sites/walmart_careers/static/icons/spark-yellow.svg new file mode 100644 index 00000000..8cfd8cce --- /dev/null +++ b/sites/walmart_careers/static/icons/spark-yellow.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/sites/walmart_careers/static/icons/spark.svg b/sites/walmart_careers/static/icons/spark.svg new file mode 100644 index 00000000..8a1f4a3e --- /dev/null +++ b/sites/walmart_careers/static/icons/spark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/sites/walmart_careers/static/icons/tile-card.svg b/sites/walmart_careers/static/icons/tile-card.svg new file mode 100644 index 00000000..fff61e75 --- /dev/null +++ b/sites/walmart_careers/static/icons/tile-card.svg @@ -0,0 +1,3 @@ + + + diff --git a/sites/walmart_careers/static/icons/tile-graduation.svg b/sites/walmart_careers/static/icons/tile-graduation.svg new file mode 100644 index 00000000..aaa269a4 --- /dev/null +++ b/sites/walmart_careers/static/icons/tile-graduation.svg @@ -0,0 +1,3 @@ + + + diff --git a/sites/walmart_careers/static/icons/tile-growth.svg b/sites/walmart_careers/static/icons/tile-growth.svg new file mode 100644 index 00000000..cdb231e1 --- /dev/null +++ b/sites/walmart_careers/static/icons/tile-growth.svg @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/sites/walmart_careers/static/icons/tile-walmart-plus.svg b/sites/walmart_careers/static/icons/tile-walmart-plus.svg new file mode 100644 index 00000000..8968fb4a --- /dev/null +++ b/sites/walmart_careers/static/icons/tile-walmart-plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/sites/walmart_careers/static/js/.gitkeep b/sites/walmart_careers/static/js/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/sites/walmart_careers/static/js/navigation.js b/sites/walmart_careers/static/js/navigation.js new file mode 100644 index 00000000..c7e01e46 --- /dev/null +++ b/sites/walmart_careers/static/js/navigation.js @@ -0,0 +1,56 @@ +(() => { + const menus = Array.from( + document.querySelectorAll(".site-header details.nav-menu, .site-header details.user-menu") + ); + + if (menus.length === 0) return; + + const setExpanded = (menu) => { + const summary = menu.querySelector(":scope > summary"); + if (summary) summary.setAttribute("aria-expanded", String(menu.open)); + }; + + const closeMenus = (except = null) => { + for (const menu of menus) { + if (menu !== except && menu.open) { + menu.open = false; + setExpanded(menu); + } + } + }; + + for (const menu of menus) { + const summary = menu.querySelector(":scope > summary"); + if (!summary) continue; + + setExpanded(menu); + summary.addEventListener("click", () => { + if (!menu.open) closeMenus(menu); + }); + menu.addEventListener("toggle", () => { + if (menu.open) closeMenus(menu); + setExpanded(menu); + }); + } + + document.addEventListener("pointerdown", (event) => { + if (!menus.some((menu) => menu.contains(event.target))) closeMenus(); + }); + + document.addEventListener("focusin", (event) => { + if (!menus.some((menu) => menu.contains(event.target))) closeMenus(); + }); + + document.addEventListener("keydown", (event) => { + if (event.key !== "Escape") return; + + const openMenu = menus.find((menu) => menu.open); + if (!openMenu) return; + + const restoreFocus = openMenu.contains(document.activeElement); + const summary = openMenu.querySelector(":scope > summary"); + closeMenus(); + if (restoreFocus && summary) summary.focus(); + event.preventDefault(); + }); +})(); diff --git a/sites/walmart_careers/tasks.jsonl b/sites/walmart_careers/tasks.jsonl new file mode 100644 index 00000000..b3b39427 --- /dev/null +++ b/sites/walmart_careers/tasks.jsonl @@ -0,0 +1,20 @@ +{"web_name":"Walmart Careers","id":"Walmart Careers--0","ques":"Search for Optician roles and open the posting at the Neighborhood Market in Wichita, KS. Report its requisition ID and the street address shown on the posting.","web":"http://localhost:40023/","upstream_url":"https://careers.walmart.com/us/en/results?searchQuery=optician","verifier_path":"sites/walmart_careers/verify/verify_0.py","judge_rubric":"Evaluate only positive evidence in the complete recorded trajectory, decoded screenshots, final answer, and supplied database snapshots. A checkpoint passes only when the required evidence is present; missing, truncated, below-the-fold, or unverified evidence does not pass. Require the login or registration, career-area/results filters, comparison detail pages, application/save/profile steps, and final confirmation pages explicitly requested by the task, in the stated order. Require every reported fact to be affirmatively associated with the correct posting and reject negated, contradictory, reassigned, or substring-only values. The before/after snapshots must preserve every row and schema object. The deterministic verifier is the primary grading contract; do not add requirements absent from the task."} +{"web_name":"Walmart Careers","id":"Walmart Careers--1","ques":"Find the Staff, Software Engineer - Backend / ML posting located in Sunnyvale, CA and quote the exact text of \"Option 2\" under Minimum Qualifications.","web":"http://localhost:40023/","upstream_url":"https://careers.walmart.com/us/en/jobs/R-2463275","verifier_path":"sites/walmart_careers/verify/verify_1.py","judge_rubric":"Evaluate only positive evidence in the complete recorded trajectory, decoded screenshots, final answer, and supplied database snapshots. A checkpoint passes only when the required evidence is present; missing, truncated, below-the-fold, or unverified evidence does not pass. Require the login or registration, career-area/results filters, comparison detail pages, application/save/profile steps, and final confirmation pages explicitly requested by the task, in the stated order. Require every reported fact to be affirmatively associated with the correct posting and reject negated, contradictory, reassigned, or substring-only values. The before/after snapshots must preserve every row and schema object. The deterministic verifier is the primary grading contract; do not add requirements absent from the task."} +{"web_name":"Walmart Careers","id":"Walmart Careers--2","ques":"Open the Freight Handler posting at eComm Whse Logistics #9054 in Porterville, CA. What shift start window does it list, and how many open positions does it show?","web":"http://localhost:40023/","upstream_url":"https://careers.walmart.com/us/en/jobs/CP-9054-11013","verifier_path":"sites/walmart_careers/verify/verify_2.py","judge_rubric":"Evaluate only positive evidence in the complete recorded trajectory, decoded screenshots, final answer, and supplied database snapshots. A checkpoint passes only when the required evidence is present; missing, truncated, below-the-fold, or unverified evidence does not pass. Require the login or registration, career-area/results filters, comparison detail pages, application/save/profile steps, and final confirmation pages explicitly requested by the task, in the stated order. Require every reported fact to be affirmatively associated with the correct posting and reject negated, contradictory, reassigned, or substring-only values. The before/after snapshots must preserve every row and schema object. The deterministic verifier is the primary grading contract; do not add requirements absent from the task."} +{"web_name":"Walmart Careers","id":"Walmart Careers--3","ques":"From the Healthcare career area page, go to its open roles and open the Pharmacy Technician posting in Bentonville, AR. What hashtag appears at the end of the \"What you'll bring\" section, and how many open positions does the posting list?","web":"http://localhost:40023/","upstream_url":"https://careers.walmart.com/us/en/home/careers-areas/healthcare","verifier_path":"sites/walmart_careers/verify/verify_3.py","judge_rubric":"Evaluate only positive evidence in the complete recorded trajectory, decoded screenshots, final answer, and supplied database snapshots. A checkpoint passes only when the required evidence is present; missing, truncated, below-the-fold, or unverified evidence does not pass. Require the login or registration, career-area/results filters, comparison detail pages, application/save/profile steps, and final confirmation pages explicitly requested by the task, in the stated order. Require every reported fact to be affirmatively associated with the correct posting and reject negated, contradictory, reassigned, or substring-only values. The before/after snapshots must preserve every row and schema object. The deterministic verifier is the primary grading contract; do not add requirements absent from the task."} +{"web_name":"Walmart Careers","id":"Walmart Careers--4","ques":"Filter to Sam's Club, Part time roles, and Weekend Overnight. Among the displayed roles whose posted pay range tops out at $20.00/hr or less, open the one in Texas and report its requisition ID and number of open positions.","web":"http://localhost:40023/","upstream_url":"https://careers.walmart.com/us/en/results?searchQuery=All","verifier_path":"sites/walmart_careers/verify/verify_4.py","judge_rubric":"Evaluate only positive evidence in the complete recorded trajectory, decoded screenshots, final answer, and supplied database snapshots. A checkpoint passes only when the required evidence is present; missing, truncated, below-the-fold, or unverified evidence does not pass. Require the login or registration, career-area/results filters, comparison detail pages, application/save/profile steps, and final confirmation pages explicitly requested by the task, in the stated order. Require every reported fact to be affirmatively associated with the correct posting and reject negated, contradictory, reassigned, or substring-only values. The before/after snapshots must preserve every row and schema object. The deterministic verifier is the primary grading contract; do not add requirements absent from the task."} +{"web_name":"Walmart Careers","id":"Walmart Careers--5","ques":"Find Full time Technology roles in Hoboken, NJ whose salary range tops out above $200,000. Open the matching posting and report its requisition ID and the degree named in \"Option 1\" of its Minimum Qualifications.","web":"http://localhost:40023/","upstream_url":"https://careers.walmart.com/us/en/results?searchQuery=All&careerareas=Technology","verifier_path":"sites/walmart_careers/verify/verify_5.py","judge_rubric":"Evaluate only positive evidence in the complete recorded trajectory, decoded screenshots, final answer, and supplied database snapshots. A checkpoint passes only when the required evidence is present; missing, truncated, below-the-fold, or unverified evidence does not pass. Require the login or registration, career-area/results filters, comparison detail pages, application/save/profile steps, and final confirmation pages explicitly requested by the task, in the stated order. Require every reported fact to be affirmatively associated with the correct posting and reject negated, contradictory, reassigned, or substring-only values. The before/after snapshots must preserve every row and schema object. The deterministic verifier is the primary grading contract; do not add requirements absent from the task."} +{"web_name":"Walmart Careers","id":"Walmart Careers--6","ques":"Set your location to Cleveland, OH within 25 miles, filter to Full time roles on a Weekday Day shift, and open the Online Order Filling Team Supervisor posting. Report the street address and the number of open positions.","web":"http://localhost:40023/","upstream_url":"https://careers.walmart.com/us/en/results?searchQuery=All","verifier_path":"sites/walmart_careers/verify/verify_6.py","judge_rubric":"Evaluate only positive evidence in the complete recorded trajectory, decoded screenshots, final answer, and supplied database snapshots. A checkpoint passes only when the required evidence is present; missing, truncated, below-the-fold, or unverified evidence does not pass. Require the login or registration, career-area/results filters, comparison detail pages, application/save/profile steps, and final confirmation pages explicitly requested by the task, in the stated order. Require every reported fact to be affirmatively associated with the correct posting and reject negated, contradictory, reassigned, or substring-only values. The before/after snapshots must preserve every row and schema object. The deterministic verifier is the primary grading contract; do not add requirements absent from the task."} +{"web_name":"Walmart Careers","id":"Walmart Careers--7","ques":"Filter to the Students career area, the Intern employment type and the Sam's Club brand, then open the Merchandising Intern posting at the Sam's Club Home Office in Bentonville, AR. Report the worker type chip shown on the posting and the street address listed for its location.","web":"http://localhost:40023/","upstream_url":"https://careers.walmart.com/us/en/results?searchQuery=All","verifier_path":"sites/walmart_careers/verify/verify_7.py","judge_rubric":"Evaluate only positive evidence in the complete recorded trajectory, decoded screenshots, final answer, and supplied database snapshots. A checkpoint passes only when the required evidence is present; missing, truncated, below-the-fold, or unverified evidence does not pass. Require the login or registration, career-area/results filters, comparison detail pages, application/save/profile steps, and final confirmation pages explicitly requested by the task, in the stated order. Require every reported fact to be affirmatively associated with the correct posting and reject negated, contradictory, reassigned, or substring-only values. The before/after snapshots must preserve every row and schema object. The deterministic verifier is the primary grading contract; do not add requirements absent from the task."} +{"web_name":"Walmart Careers","id":"Walmart Careers--8","ques":"There are Auto Care Center Technician postings at two Mississippi stores. Open both and report which store number has more open positions and how many.","web":"http://localhost:40023/","upstream_url":"https://careers.walmart.com/us/en/results?searchQuery=auto+care+center+technician","verifier_path":"sites/walmart_careers/verify/verify_8.py","judge_rubric":"Evaluate only positive evidence in the complete recorded trajectory, decoded screenshots, final answer, and supplied database snapshots. A checkpoint passes only when the required evidence is present; missing, truncated, below-the-fold, or unverified evidence does not pass. Require the login or registration, career-area/results filters, comparison detail pages, application/save/profile steps, and final confirmation pages explicitly requested by the task, in the stated order. Require every reported fact to be affirmatively associated with the correct posting and reject negated, contradictory, reassigned, or substring-only values. The before/after snapshots must preserve every row and schema object. The deterministic verifier is the primary grading contract; do not add requirements absent from the task."} +{"web_name":"Walmart Careers","id":"Walmart Careers--9","ques":"Two Freight Handler postings are located in Marcy, NY at different facilities. Which one has the earlier shift start time? Report its requisition ID and that start window.","web":"http://localhost:40023/","upstream_url":"https://careers.walmart.com/us/en/results?searchQuery=freight+handler","verifier_path":"sites/walmart_careers/verify/verify_9.py","judge_rubric":"Evaluate only positive evidence in the complete recorded trajectory, decoded screenshots, final answer, and supplied database snapshots. A checkpoint passes only when the required evidence is present; missing, truncated, below-the-fold, or unverified evidence does not pass. Require the login or registration, career-area/results filters, comparison detail pages, application/save/profile steps, and final confirmation pages explicitly requested by the task, in the stated order. Require every reported fact to be affirmatively associated with the correct posting and reject negated, contradictory, reassigned, or substring-only values. The before/after snapshots must preserve every row and schema object. The deterministic verifier is the primary grading contract; do not add requirements absent from the task."} +{"web_name":"Walmart Careers","id":"Walmart Careers--10","ques":"Compare the Senior Manager, Delivery Search, Arrival & Matching (Last Mile Delivery) postings in Bentonville, AR and Hoboken, NJ. Which one requires more years of experience under \"Option 2\" of its Minimum Qualifications? Report that posting's requisition ID and the number of years.","web":"http://localhost:40023/","upstream_url":"https://careers.walmart.com/us/en/jobs/R-2435546","verifier_path":"sites/walmart_careers/verify/verify_10.py","judge_rubric":"Evaluate only positive evidence in the complete recorded trajectory, decoded screenshots, final answer, and supplied database snapshots. A checkpoint passes only when the required evidence is present; missing, truncated, below-the-fold, or unverified evidence does not pass. Require the login or registration, career-area/results filters, comparison detail pages, application/save/profile steps, and final confirmation pages explicitly requested by the task, in the stated order. Require every reported fact to be affirmatively associated with the correct posting and reject negated, contradictory, reassigned, or substring-only values. The before/after snapshots must preserve every row and schema object. The deterministic verifier is the primary grading contract; do not add requirements absent from the task."} +{"web_name":"Walmart Careers","id":"Walmart Careers--11","ques":"Log in with the demo account (email: alice.j@test.com, password: TestPass123!), search for Yard Driver-Off Property roles and save the Williamsburg, VA posting to your Saved roles.","web":"http://localhost:40023/","upstream_url":"https://careers.walmart.com/us/en/candidate-home/saved-roles","verifier_path":"sites/walmart_careers/verify/verify_11.py","judge_rubric":"Evaluate only positive evidence in the complete recorded trajectory, decoded screenshots, final answer, and supplied database snapshots. A checkpoint passes only when the required evidence is present; missing, truncated, below-the-fold, or unverified evidence does not pass. Require the login or registration, career-area/results filters, comparison detail pages, application/save/profile steps, and final confirmation pages explicitly requested by the task, in the stated order. Require every reported fact to be affirmatively associated with the correct posting and reject negated, contradictory, reassigned, or substring-only values. The before/after snapshots must contain exactly the requested persistent mutation and preserve every unrelated row and schema object. The deterministic verifier is the primary grading contract; do not add requirements absent from the task."} +{"web_name":"Walmart Careers","id":"Walmart Careers--12","ques":"Log in as bob.c@test.com (password: TestPass123!), open your Saved roles, and remove the saved role that is at a Neighborhood Market store.","web":"http://localhost:40023/","upstream_url":"https://careers.walmart.com/us/en/candidate-home/saved-roles","verifier_path":"sites/walmart_careers/verify/verify_12.py","judge_rubric":"Evaluate only positive evidence in the complete recorded trajectory, decoded screenshots, final answer, and supplied database snapshots. A checkpoint passes only when the required evidence is present; missing, truncated, below-the-fold, or unverified evidence does not pass. Require the login or registration, career-area/results filters, comparison detail pages, application/save/profile steps, and final confirmation pages explicitly requested by the task, in the stated order. Require every reported fact to be affirmatively associated with the correct posting and reject negated, contradictory, reassigned, or substring-only values. The before/after snapshots must contain exactly the requested persistent mutation and preserve every unrelated row and schema object. The deterministic verifier is the primary grading contract; do not add requirements absent from the task."} +{"web_name":"Walmart Careers","id":"Walmart Careers--13","ques":"Log in as carol.d@test.com (password: TestPass123!), apply to the Pharmacy Technician posting in Tacoma, WA using phone number 253-555-0142, and report the confirmation number shown after submitting.","web":"http://localhost:40023/","upstream_url":"https://careers.walmart.com/us/en/jobs/contact-details?from=%2Fus%2Fen%2Fjobs%2Fapply&lang=en&country=us","verifier_path":"sites/walmart_careers/verify/verify_13.py","judge_rubric":"Evaluate only positive evidence in the complete recorded trajectory, decoded screenshots, final answer, and supplied database snapshots. A checkpoint passes only when the required evidence is present; missing, truncated, below-the-fold, or unverified evidence does not pass. Require the login or registration, career-area/results filters, comparison detail pages, application/save/profile steps, and final confirmation pages explicitly requested by the task, in the stated order. Require every reported fact to be affirmatively associated with the correct posting and reject negated, contradictory, reassigned, or substring-only values. The before/after snapshots must contain exactly the requested persistent mutation and preserve every unrelated row and schema object. The deterministic verifier is the primary grading contract; do not add requirements absent from the task."} +{"web_name":"Walmart Careers","id":"Walmart Careers--14","ques":"Register a new account with an email and password of your choice, then save the eCom Warehouse Worker posting at eComm Whse Logistics #9046 in Marcy, NY to your Saved roles.","web":"http://localhost:40023/","upstream_url":"https://careers.walmart.com/us/en/jobs/CP-9046-11101","verifier_path":"sites/walmart_careers/verify/verify_14.py","judge_rubric":"Evaluate only positive evidence in the complete recorded trajectory, decoded screenshots, final answer, and supplied database snapshots. A checkpoint passes only when the required evidence is present; missing, truncated, below-the-fold, or unverified evidence does not pass. Require the login or registration, career-area/results filters, comparison detail pages, application/save/profile steps, and final confirmation pages explicitly requested by the task, in the stated order. Require every reported fact to be affirmatively associated with the correct posting and reject negated, contradictory, reassigned, or substring-only values. The before/after snapshots must contain exactly the requested persistent mutation and preserve every unrelated row and schema object. The deterministic verifier is the primary grading contract; do not add requirements absent from the task."} +{"web_name":"Walmart Careers","id":"Walmart Careers--15","ques":"Log in as david.k@test.com (password: TestPass123!) and update your account so the city is Rogers and the state is AR, then open My applications and report the confirmation number of your existing application.","web":"http://localhost:40023/","upstream_url":"https://careers.walmart.com/us/en/candidate-home","verifier_path":"sites/walmart_careers/verify/verify_15.py","judge_rubric":"Evaluate only positive evidence in the complete recorded trajectory, decoded screenshots, final answer, and supplied database snapshots. A checkpoint passes only when the required evidence is present; missing, truncated, below-the-fold, or unverified evidence does not pass. Require the login or registration, career-area/results filters, comparison detail pages, application/save/profile steps, and final confirmation pages explicitly requested by the task, in the stated order. Require every reported fact to be affirmatively associated with the correct posting and reject negated, contradictory, reassigned, or substring-only values. The before/after snapshots must contain exactly the requested persistent mutation and preserve every unrelated row and schema object. The deterministic verifier is the primary grading contract; do not add requirements absent from the task."} +{"web_name":"Walmart Careers","id":"Walmart Careers--16","ques":"Set the location to Puerto Rico, then use the Shift filter for Weekday Day and the Rate filter for Hourly. Among the Cashier postings, report the requisition ID of the one with the most open positions and that number.","web":"http://localhost:40023/","upstream_url":"https://careers.walmart.com/us/en/results?searchQuery=cashier","verifier_path":"sites/walmart_careers/verify/verify_16.py","judge_rubric":"Evaluate only positive evidence in the complete recorded trajectory, decoded screenshots, final answer, and supplied database snapshots. A checkpoint passes only when the required evidence is present; missing, truncated, below-the-fold, or unverified evidence does not pass. Require the login or registration, career-area/results filters, comparison detail pages, application/save/profile steps, and final confirmation pages explicitly requested by the task, in the stated order. Require every reported fact to be affirmatively associated with the correct posting and reject negated, contradictory, reassigned, or substring-only values. The before/after snapshots must preserve every row and schema object. The deterministic verifier is the primary grading contract; do not add requirements absent from the task."} +{"web_name":"Walmart Careers","id":"Walmart Careers--17","ques":"Log in as alice.j@test.com (password: TestPass123!). Exactly one of your saved roles is a Part time job; apply to it with your account email, then report that posting's shift start window and your confirmation number.","web":"http://localhost:40023/","upstream_url":"https://careers.walmart.com/us/en/candidate-home/saved-roles","verifier_path":"sites/walmart_careers/verify/verify_17.py","judge_rubric":"Evaluate only positive evidence in the complete recorded trajectory, decoded screenshots, final answer, and supplied database snapshots. A checkpoint passes only when the required evidence is present; missing, truncated, below-the-fold, or unverified evidence does not pass. Require the login or registration, career-area/results filters, comparison detail pages, application/save/profile steps, and final confirmation pages explicitly requested by the task, in the stated order. Require every reported fact to be affirmatively associated with the correct posting and reject negated, contradictory, reassigned, or substring-only values. The before/after snapshots must contain exactly the requested persistent mutation and preserve every unrelated row and schema object. The deterministic verifier is the primary grading contract; do not add requirements absent from the task."} +{"web_name":"Walmart Careers","id":"Walmart Careers--18","ques":"Open the Supply Chain and Transportation career area page, go to its Drivers category, and compare the Class A CDL Truck Driver postings at the Ottawa, KS and Williamsburg, VA facilities. For whichever of the two lists more open positions, report its requisition ID, its shift start window and that number of open positions.","web":"http://localhost:40023/","upstream_url":"https://careers.walmart.com/us/en/home/careers-areas/supply-chain-and-transportation","verifier_path":"sites/walmart_careers/verify/verify_18.py","judge_rubric":"Evaluate only positive evidence in the complete recorded trajectory, decoded screenshots, final answer, and supplied database snapshots. A checkpoint passes only when the required evidence is present; missing, truncated, below-the-fold, or unverified evidence does not pass. Require the login or registration, career-area/results filters, comparison detail pages, application/save/profile steps, and final confirmation pages explicitly requested by the task, in the stated order. Require every reported fact to be affirmatively associated with the correct posting and reject negated, contradictory, reassigned, or substring-only values. The before/after snapshots must preserve every row and schema object. The deterministic verifier is the primary grading contract; do not add requirements absent from the task."} +{"web_name":"Walmart Careers","id":"Walmart Careers--19","ques":"Log in as bob.c@test.com (password: TestPass123!). From the Stores and Clubs career area page, open the Digital Pickup and Delivery category, filter it to Full time roles, and open the posting with the fewest open positions. Save that role to your Saved roles, then report its requisition ID and the street address shown on the posting.","web":"http://localhost:40023/","upstream_url":"https://careers.walmart.com/us/en/home/careers-areas/stores-and-clubs","verifier_path":"sites/walmart_careers/verify/verify_19.py","judge_rubric":"Evaluate only positive evidence in the complete recorded trajectory, decoded screenshots, final answer, and supplied database snapshots. A checkpoint passes only when the required evidence is present; missing, truncated, below-the-fold, or unverified evidence does not pass. Require the login or registration, career-area/results filters, comparison detail pages, application/save/profile steps, and final confirmation pages explicitly requested by the task, in the stated order. Require every reported fact to be affirmatively associated with the correct posting and reject negated, contradictory, reassigned, or substring-only values. The before/after snapshots must contain exactly the requested persistent mutation and preserve every unrelated row and schema object. The deterministic verifier is the primary grading contract; do not add requirements absent from the task."} diff --git a/sites/walmart_careers/templates/404.html b/sites/walmart_careers/templates/404.html new file mode 100644 index 00000000..4288a424 --- /dev/null +++ b/sites/walmart_careers/templates/404.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} +{% block title %}Page not found | Walmart Careers{% endblock %} +{% block content %} +
+
+
+

We couldn't find that page

+

The role or page you were looking for isn't here. Try searching for a role instead.

+ Browse open roles + Back to home +
+
+
+{% endblock %} diff --git a/sites/walmart_careers/templates/500.html b/sites/walmart_careers/templates/500.html new file mode 100644 index 00000000..2083c345 --- /dev/null +++ b/sites/walmart_careers/templates/500.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} +{% block title %}Service unavailable | Walmart Careers{% endblock %} +{% block content %} +
+
+

We could not complete that request

+

The local Walmart Careers mirror encountered an internal error. No external application was submitted.

+ Return home +
+
+{% endblock %} diff --git a/sites/walmart_careers/templates/_benefits.html b/sites/walmart_careers/templates/_benefits.html new file mode 100644 index 00000000..aa44d5b7 --- /dev/null +++ b/sites/walmart_careers/templates/_benefits.html @@ -0,0 +1,29 @@ +{# + Shared benefits block: the three coloured tiles, the two-column icon rows and a + button. The job detail page ("Benefits you'll enjoy") and the career-area pages + ("Explore our benefits") both render this partial so the two stay identical. +#} +{% macro benefits_block(heading, tiles, rows, button_label, button_href, section_id=None) -%} +
+

{{ heading }}

+
+ {% for name, headline, blurb, icon in tiles %} +
+ + {{ name }} + {{ headline }} +

{{ blurb }}

+
+ {% endfor %} +
+
+ {% for name, blurb, icon in rows %} +
+ +
{{ name }}{{ blurb }}
+
+ {% endfor %} +
+

{{ button_label }}

+
+{%- endmacro %} diff --git a/sites/walmart_careers/templates/_job_card.html b/sites/walmart_careers/templates/_job_card.html new file mode 100644 index 00000000..ad3b2eaf --- /dev/null +++ b/sites/walmart_careers/templates/_job_card.html @@ -0,0 +1,46 @@ +{# + Result card. The lines mirror the live site: a salaried posting shows only its + title, "City, ST" and the annual pay range; an hourly posting adds the + "banner #store" line, the ZIP and the shift label. Everything else (street, + requisition ID, open positions, shift window, qualifications) is detail-only. + + The whole card is clickable (stretched title link) and carries one outlined + "Select +" pill, exactly like upstream. `compact` drops the pill (trending + strips); `unsave` adds the "Saved" toggle used on the saved-roles page. +#} +{% macro job_card(job, saved_ids, compact=False, unsave=False) -%} +
+ {{ job.brand }} +
+

{{ job.title }}

+
+ {% if job.is_salaried %} +
{{ job.store.city }}, {{ job.store.state }}
+ {% else %} + {% if not job.store.is_office %} +
{{ job.store.banner }} #{{ job.store.store_number }}
+ {% endif %} +
{{ job.store.city }}, {{ job.store.state }}  {{ job.store.zip }}
+ {% endif %} +
+
+ {%- if not job.is_salaried %}{{ job.shift_label }} • {% endif -%} + {{ job.pay_range }} +
+ {% if not compact %} +
+ Select + + {% if unsave %} +
+ + + +
+ {% endif %} +
+ {% endif %} +
+
+{%- endmacro %} diff --git a/sites/walmart_careers/templates/_life.html b/sites/walmart_careers/templates/_life.html new file mode 100644 index 00000000..4535f939 --- /dev/null +++ b/sites/walmart_careers/templates/_life.html @@ -0,0 +1,23 @@ +{# + "Life at Walmart": heading, lead sentence, the two-column text / photo grid, + the full-bleed blue quote band and the closing lines. `life` is one of the + content.LIFE_AT_WALMART_* dicts; shared by both detail layouts and About Us. +#} +{% macro life_block(heading, life, section_id='life-at-walmart') -%} +
+

{{ heading }}

+

{{ life.lead }}

+
+
+ {% for paragraph in life.left %}

{{ paragraph }}

{% endfor %} +
+
+ + {% for paragraph in life.right %}

{{ paragraph }}

{% endfor %} +
+
+

{{ life.band }}

+

{{ life.closing }}

+

{{ life.note }}

+
+{%- endmacro %} diff --git a/sites/walmart_careers/templates/about.html b/sites/walmart_careers/templates/about.html new file mode 100644 index 00000000..022fe7f1 --- /dev/null +++ b/sites/walmart_careers/templates/about.html @@ -0,0 +1,56 @@ +{% extends "base.html" %} +{% from "_life.html" import life_block %} +{% block body_class %}l1{% endblock %} +{% block title %}About Us | Walmart Careers{% endblock %} +{% block content %} +
+
+

{{ content.ABOUT_HEADING }}

+

{{ content.ABOUT_BLURB }}

+
+
+ +
+
+
+ {% for heading, body in content.ABOUT_SECTIONS %} +
+

{{ heading }}

+

{{ body }}

+
+ {% endfor %} +
+
+
+ +
+ {{ life_block(content.LIFE_AT_WALMART_HEADING, content.LIFE_AT_WALMART_FIELD) }} +
+ +
+
+

Guided by our values

+
+ {% for title, blurb in content.VALUES %} +

{{ title }}

{{ blurb }}

+ {% endfor %} +
+
+
+ +
+
+

Explore our career areas

+
+ {% for area in areas %} + + {{ area.name }} + + + {% endfor %} +
+
+
+{% endblock %} diff --git a/sites/walmart_careers/templates/account.html b/sites/walmart_careers/templates/account.html new file mode 100644 index 00000000..6b4d5445 --- /dev/null +++ b/sites/walmart_careers/templates/account.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} +{% block title %}My account | Walmart Careers{% endblock %} +{% block content %} +
+
+

{{ current_user.display_name }}

+
    +
  • Email{{ current_user.email }}
  • +
  • First name{{ current_user.first_name }}
  • +
  • Last name{{ current_user.last_name }}
  • +
  • Phone number{{ current_user.phone or '—' }}
  • +
  • City{{ current_user.city or '—' }}
  • +
  • State{{ current_user.state or '—' }}
  • +
  • Saved roles{{ saved_count }}
  • +
  • Applications{{ application_count }}
  • +
+ Edit profile + Saved roles + My applications +
+
+{% endblock %} diff --git a/sites/walmart_careers/templates/account_edit.html b/sites/walmart_careers/templates/account_edit.html new file mode 100644 index 00000000..8010b1fa --- /dev/null +++ b/sites/walmart_careers/templates/account_edit.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} +{% block title %}Edit profile | Walmart Careers{% endblock %} +{% block content %} +
+
+

Edit your profile

+ {% if errors %} + + {% endif %} +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + Cancel +
+
+
+{% endblock %} diff --git a/sites/walmart_careers/templates/applications.html b/sites/walmart_careers/templates/applications.html new file mode 100644 index 00000000..b3eae8c1 --- /dev/null +++ b/sites/walmart_careers/templates/applications.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} +{% block title %}My applications | Walmart Careers{% endblock %} +{% block content %} +
+
+

My applications ({{ rows|length }})

+ {% if rows %} +
+ + + + + + + {% for row in rows %} + + + + + + + {% endfor %} + +
Submitted applications
RoleLocationConfirmation numberStatus
{{ row.job.title }}{{ row.job.store.city }}, {{ row.job.store.state }}{{ row.confirmation_no }}{{ row.status }}
+
+ {% else %} +
+

No applications yet

+

Once you submit an application it shows up here with its confirmation number.

+ Browse open roles +
+ {% endif %} +
+
+{% endblock %} diff --git a/sites/walmart_careers/templates/apply_confirm.html b/sites/walmart_careers/templates/apply_confirm.html new file mode 100644 index 00000000..3da18b9b --- /dev/null +++ b/sites/walmart_careers/templates/apply_confirm.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} +{% block title %}Review your application | Walmart Careers{% endblock %} +{% block content %} +
+
+
+

Review your application

+

Step 2 of 2 — check your details, then submit.

+
    +
  • Role{{ job.title }}
  • +
  • Location{{ job.store.banner }} #{{ job.store.store_number }}, {{ job.store.city }}, {{ job.store.state }}
  • +
  • Requisition ID{{ job.job_id }}
  • +
  • Email{{ draft.email }}
  • +
  • Name{{ draft.first_name }} {{ draft.last_name }}
  • +
  • Phone{{ draft.phone }}
  • +
+
+ +
+ Edit details + +
+
+
+
+{% endblock %} diff --git a/sites/walmart_careers/templates/apply_contact.html b/sites/walmart_careers/templates/apply_contact.html new file mode 100644 index 00000000..69bebfb9 --- /dev/null +++ b/sites/walmart_careers/templates/apply_contact.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} +{% block title %}Apply to {{ job.title }} | Walmart Careers{% endblock %} +{% block content %} +
+
+
+

{{ content.APPLY_HEADING }}

+

Apply: {{ job.title }} — {{ job.store.banner }} #{{ job.store.store_number }}, + {{ job.store.city }}, {{ job.store.state }} — Step 1 of 2: contact details

+ {% if errors %} + + {% endif %} +
+ +
+ + +

{{ content.APPLY_EMAIL_HINT }}

+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ +
+
+ Back to the role + +
+
+
+
+{% endblock %} diff --git a/sites/walmart_careers/templates/apply_submitted.html b/sites/walmart_careers/templates/apply_submitted.html new file mode 100644 index 00000000..add9d830 --- /dev/null +++ b/sites/walmart_careers/templates/apply_submitted.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% block title %}Application submitted | Walmart Careers{% endblock %} +{% block content %} +
+
+
+

Application submitted

+

Thanks, {{ application.first_name }}. Your application for {{ job.title }} at + {{ job.store.banner }} #{{ job.store.store_number }} in {{ job.store.city }}, {{ job.store.state }} + has been received.

+
Confirmation number: {{ application.confirmation_no }}
+
    +
  • Status{{ application.status }}
  • +
  • Email{{ application.email }}
  • +
  • Phone{{ application.phone }}
  • +
+
+ Keep browsing roles + {% if current_user.is_authenticated %} + My applications + {% endif %} +
+
+
+{% endblock %} diff --git a/sites/walmart_careers/templates/area.html b/sites/walmart_careers/templates/area.html new file mode 100644 index 00000000..af4dff26 --- /dev/null +++ b/sites/walmart_careers/templates/area.html @@ -0,0 +1,225 @@ +{% extends "base.html" %} +{% from "_benefits.html" import benefits_block %} +{% block body_class %}l1{% endblock %} +{% block title %}{{ area.name }} careers | Walmart Careers{% endblock %} +{% block content %} +{% set page = content.AREA_PAGE.get(area.slug, content.AREA_PAGE_DEFAULT) %} +{% set hero_image = 'military-banner.png' if area.slug == 'Military' else area.hero_image %} +{% set roles_url = url_for('results', area=area.slug) if area.is_filterable else url_for('results') %} +{% set office_area = area.slug in ('technology', 'corporate') %} +{% set military = area.slug == 'Military' %} + +
+ + +
+ +{% if categories %} +
+
+

Join our team

+
+
+ + + {{ area.name }} associates +
+ +
+
+
+{% elif military %} +{# The live Military page opens with two feature rows and three program tiles. #} +
+
+ {% for heading, blurb, cta, photo in content.MILITARY_FEATURES %} +
+ +
+

{{ heading }}

+

{{ blurb }}

+ {{ 'Browse all open roles' if military else cta }} +
+
+ {% endfor %} +
+ {% for heading, blurb, photo in content.MILITARY_PROGRAMS %} + {% set target = url_for('register') if loop.last else (url_for('results', area='students', type='Intern') if loop.index == 2 else roles_url) %} + + + {{ heading }} +

{{ blurb }}

+ +
+ {% endfor %} +
+
+
+{% else %} +
+
+
+

Programs, not a job family

+

{{ area.name }} hiring runs through every career area on this site. Browse open roles and filter + by the career area, brand, shift or location that fits you.

+ Browse all open roles +
+
+
+{% endif %} + +{% set benefits_html %} +
+ {{ benefits_block('Explore our benefits', + content.benefit_tiles_for('Walmart', 'salaried' if office_area else 'hourly'), + content.JOB_BENEFIT_ROWS, 'Learn more about benefits', url_for('about_us')) }} +
+{% endset %} + +{# Field areas show the benefits right after "Join our team"; Military after its stories strip. #} +{% if not military %}{{ benefits_html }}{% endif %} + +{% if office_area %} +
+

{{ page.quote }}

+
+{% endif %} + +
+
+
+
+ + {{ page.tiles[0] }} + +
+ +
+ {% if page.tiles|length > 2 %} +
+ +
+ + {{ page.tiles[1] }} + + + {{ page.tiles[2] }} + + {% else %} + + + {{ page.tiles[1] }} + + {% endif %} +
+
+

{{ page.headline }}

+ {{ page.cta }} +
+
+
+
+ +{% if area.slug == 'stores-and-clubs' %} +{% set meet = content.MEET_STORE_COACH %} +
+
+
+
+ +
{{ meet.kicker }}{{ meet.role }}
+
+
+

Meet {{ meet.name }}

+

{{ meet.blurb }}

+
+
+
+
+{% endif %} + +{% if not office_area %} +
+

{{ page.quote }}

+
+{% endif %} + +{% if office_area %} +
+
+

{{ page.hubs_heading }}

+

{{ page.hubs_blurb }}

+ +

See all hubs

+
+
+{% else %} +
+
+

{{ content.ASSOCIATES_HEADING }}

+

{{ content.ASSOCIATES_BLURB }}

+
+ {% for name, role, _quote in page.testimonials %} +
+ +
Meet {{ name }}{{ role }}
+
+ {% endfor %} +
+
+
+{% endif %} + +{% if military %}{{ benefits_html }}{% endif %} + +{% if page.testimonials %} +
+
+

{{ content.INSPIRATION_HEADING }}

+
+ {% for name, role, quote in page.testimonials %} +
+ {{ name }}, {{ role }} +

“{{ quote }}”

+
+ {% endfor %} +
+
+
+{% endif %} + +
+
+

Find the role that's a perfect fit.

+ +
+
+{% endblock %} diff --git a/sites/walmart_careers/templates/base.html b/sites/walmart_careers/templates/base.html new file mode 100644 index 00000000..ee47ebce --- /dev/null +++ b/sites/walmart_careers/templates/base.html @@ -0,0 +1,165 @@ + + + + + + {% block title %}Careers at Walmart{% endblock %} + + + + + + + + +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} + {% endwith %} + {% block content %}{% endblock %} +
+ +
+
+ + {% set rights = 'applicant rights under Federal Employment Laws.' %} + + + +
+
+ + diff --git a/sites/walmart_careers/templates/base_auth.html b/sites/walmart_careers/templates/base_auth.html new file mode 100644 index 00000000..58961bb6 --- /dev/null +++ b/sites/walmart_careers/templates/base_auth.html @@ -0,0 +1,33 @@ +{# + Stripped layout for sign-in / register, mirroring identity.walmart.com: + a centred spark, no navigation, and a light link footer. +#} + + + + + + {% block title %}Sign in | Walmart Careers{% endblock %} + + + + +
+ + + + {% with messages = get_flashed_messages(with_categories=true) %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endwith %} + {% block content %}{% endblock %} +
+ + + diff --git a/sites/walmart_careers/templates/hiring_process.html b/sites/walmart_careers/templates/hiring_process.html new file mode 100644 index 00000000..5f9bae34 --- /dev/null +++ b/sites/walmart_careers/templates/hiring_process.html @@ -0,0 +1,70 @@ +{% extends "base.html" %} +{% from "_job_card.html" import job_card %} +{% block body_class %}l1{% endblock %} +{% block title %}How we hire | Walmart Careers{% endblock %} +{% block content %} +
+ +
+

{{ content.HIRING_HEADING }}

+

{{ content.HIRING_BLURB }}

+ {{ content.HIRING_HERO_CTA }} +
+
+ +
+
+

Explore something new

+
+ {% for title, blurb in content.HIRING_STEPS %} +

{{ title }}

{{ blurb }}

+ {% endfor %} +
+
+
+ +{% for heading, questions in content.HIRING_FAQ %} + {% set intro = content.HIRING_FAQ_INTROS[loop.index0] %} +
+
+
+
+ +

{{ heading }}

+

{{ intro[0] }}

+
+
+ {% for question, answer in questions %} +
+ {{ question }} +

{{ answer }}

+
+ {% endfor %} +
+
+
+
+{% endfor %} + +
+
+
+ +
+

{{ content.HIRING_SIMULATOR.heading }}

+

{{ content.HIRING_SIMULATOR.blurb }}

+ {{ content.HIRING_SIMULATOR.cta }} +
+
+
+
+ + +{% endblock %} diff --git a/sites/walmart_careers/templates/index.html b/sites/walmart_careers/templates/index.html new file mode 100644 index 00000000..d04d2300 --- /dev/null +++ b/sites/walmart_careers/templates/index.html @@ -0,0 +1,185 @@ +{% extends "base.html" %} +{% block body_class %}l1{% endblock %} +{% from "_job_card.html" import job_card %} +{% block title %}Careers at Walmart{% endblock %} +{% block content %} +
+
+

{{ content.HERO_HEADLINE_1 }}{{ content.HERO_HEADLINE_2 }}

+ +
+
+ Associates in a home office space + + Associates in a distribution center +
+
+ +
+
+

Trending roles

+
+ {% for job in trending %}{{ job_card(job, saved_ids, compact=True) }}{% endfor %} +
+
+
+ +
+
+
+ {% for area in ribbon_areas %} + + {{ area.name }} + + + {% endfor %} +
+
+
+ +
+
+
+
+ + Grow your career here + + +
+ An associate helping a customer in the grocery aisle +
+
+ +
+
+ A delivery drone in flight +
+ + People-led. Tech-powered. + + + + Guided by our values + + +
+ The Walmart home office campus +
+
Strive for excellence
+
+ +
+
+ +
+
Respect for the individual
+ + Sam's Club + + +
+ A Member Services associate at Sam's Club +
+
+
+

{{ content.HOME_INTRO_HEADLINE[0] }}
{{ content.HOME_INTRO_HEADLINE[1] }}

+ {{ content.HOME_INTRO_CTA }} +
+
+
+
+ +
+
+

Explore our Benefits

+
+
+ {% for name, blurb, icon in content.BENEFIT_ROWS %} +
+ + {{ name }} + {{ blurb }} +
+ {% endfor %} +
+
+

{{ content.BENEFITS_ASIDE }}

+ {{ content.BENEFITS_CTA }} +
+
+
+
+ +
+
+
+

{{ content.MILESTONE_HEADING }}

+
+ {% for sentence, label, style in content.MILESTONE_BADGES %} +
+ {% if loop.index is even %}{% endif %} +
+
{% if label %}{{ label }}{% endif %}
+
{{ sentence }}
+ {% if label != '10 YEARS' %}
Walmart
{% else %}
{{ label }}
{% endif %} +
+ {% if loop.index is odd %}{% endif %} +
+ {% endfor %} +
+
+
+
+ +
+
+

{{ content.ASSOCIATES_HEADING }}

+

{{ content.ASSOCIATES_BLURB }}

+
+ {% for role, kicker in [('Store Coach', 'Day in the life'), + ('Optician', 'Day in Life'), + ('Store Manager', 'Day in the life')] %} +
+ +
{{ kicker }}{{ role }} +
+
+ {% endfor %} +
+
+
+ +
+
+

{{ content.FIND_ROLE_HEADING }}

+ +
+
+{% endblock %} diff --git a/sites/walmart_careers/templates/job_detail.html b/sites/walmart_careers/templates/job_detail.html new file mode 100644 index 00000000..dcf77bd1 --- /dev/null +++ b/sites/walmart_careers/templates/job_detail.html @@ -0,0 +1,243 @@ +{% extends "base.html" %} +{% from "_job_card.html" import job_card %} +{% from "_benefits.html" import benefits_block %} +{% from "_life.html" import life_block %} +{% block title %}{{ job.title }} in {{ job.store.city }}, {{ job.store.state }} | Walmart Careers{% endblock %} +{% block content %} +{% set salaried = job.population == 'salaried' %} + +{# + Both populations use the live three-photo masthead; the identity card in the + middle column is solid ld-blue for salaried postings and blue-over-navy for + hourly ones, exactly as reference/job_detail_corp.png vs reference/job_detail.png. +#} +
+
+
+
+
+ Walmart spark +
+ {% if job.hero_images|length > 0 %} + + {% endif %} +
+
+ {% if job.hero_images|length > 1 %} + + {% endif %} +
+
+ + +
+
+
{{ job.title }}
+
{{ job.store.city }}, {{ job.store.state }}
+
+
+ {% if is_saved %} +
+ + + +
+ {% else %} +
+ + + +
+ {% endif %} + Apply now + +
+
+
+ {% if job.hero_images|length > 2 %} + + {% endif %} +
+
+
+ +{# + Below the masthead the live page is one two-column layout: the sticky + "On this page" panel on the left, and on the right the role details, the + benefits block and "Life at Walmart" one after another in the same column. +#} +
+ + +
+
+

{{ job.title }}

+ +
+
+ {% if not job.store.is_office %} + + {% endif %} +
+ {{ job.store.street }}
+ {{ job.store.city }}, {{ job.store.state }} {{ job.store.zip }} +
+ {% if job.positions_available %} +
{{ job.positions_available }} open position{{ '' if job.positions_available == 1 else 's' }}
+ {% endif %} + {{ job.job_id }} +
+
{{ map_svg|safe }}
+
+ +
+ + + {{ job.pay_range }}{% if job.min_age_note %}*{% endif %} + + {% if salaried %} + + + {{ job.worker_type }} + + + + Salaried + + {% else %} + + + {{ job.employment_type }} + + + + {{ job.shift_time or job.shift_label }} + + {% if job.shifts|length > 1 %} + Eligible shifts: {{ job.shifts|join(', ') }} + {% endif %} + {% endif %} +
+ {% if job.min_age_note %} +

* Must be at least 18 years old

+ {% endif %} + +
+ {% if salaried %} +

Position Summary...

+

{{ job.summary }}

+

What you'll do...

+ {% for paragraph in job.description.split('\n\n') %}

{{ paragraph }}

{% endfor %} + {% if job.about_team %} +

About the team:

+

{{ job.about_team }}

+ {% endif %} + {% if job.additional_description %} +

What you'll bring:

+
    + {% for bullet in job.additional_description %}
  • {{ bullet }}
  • {% endfor %} +
+ {% endif %} + {% set about = content.SALARIED_ABOUT_AREA.get(job.area.slug, content.SALARIED_ABOUT_DEFAULT) %} +

{{ about[0] }}

+

{{ about[1] }}

+ {% if job.store.is_office %} +

Flexible, hybrid work:

+

{{ content.SALARIED_HYBRID_NOTE }}

+ {% endif %} +

Benefits:

+

{{ content.SALARIED_BENEFITS_NOTE }}

+

Equal Opportunity Employer:

+

{{ content.SALARIED_EEO_NOTE }}

+

{{ content.SALARIED_SCOPE_NOTE }}

+

{{ content.SALARIED_PAY_NOTE }} + The annual salary range for this position is {{ job.pay_range }}. + Additional compensation includes annual or quarterly performance bonuses.

+

Minimum Qualifications...

+

{{ content.MIN_QUAL_PREAMBLE }}

+
    + {% for option in job.min_qualifications %}
  • {{ option }}
  • {% endfor %} +
+

Preferred Qualifications...

+

{{ content.PREF_QUAL_PREAMBLE }}

+

{{ job.preferred_qualifications }}

+

Primary Location...

+

{{ job.store.street }}, {{ job.store.city }}, {{ job.store.state }} {{ job.store.zip }}, + United States of America

+ {% else %} +

Role summary

+

{{ job.summary }}

+

What you'll do

+ {% for paragraph in job.description.split('\n\n') %}

{{ paragraph }}

{% endfor %} +

What you'll bring

+
    + {% for bullet in job.additional_description %}
  • {{ bullet }}
  • {% endfor %} +
+ {% if job.hashtag %} +

{{ job.hashtag }}

+ {% endif %} + + {% endif %} + +
+
+ + {{ benefits_block("Benefits you'll enjoy", benefit_tiles, content.JOB_BENEFIT_ROWS, + 'Learn more', url_for('resources_hiring'), section_id='benefits') }} + + {{ life_block(content.LIFE_AT_WALMART_HEADING, + content.LIFE_AT_WALMART_CORP if salaried and job.store.city == 'Bentonville' else content.LIFE_AT_WALMART_FIELD) }} +
+
+ +{% if related %} + +{% endif %} +{% endblock %} diff --git a/sites/walmart_careers/templates/locations.html b/sites/walmart_careers/templates/locations.html new file mode 100644 index 00000000..5905c0e5 --- /dev/null +++ b/sites/walmart_careers/templates/locations.html @@ -0,0 +1,55 @@ +{% extends "base.html" %} +{% block body_class %}l1{% endblock %} +{% block title %}Our locations | Walmart Careers{% endblock %} +{% block content %} +
+
+ +

{{ content.LOCATIONS_HEADING }}

+
+
+
+
+

{{ content.LOCATIONS_BLURB }}

+
+
+
+
+

Hubs around the world

+
+ {% for hub in hubs %} +
+ + {{ hub.hub_name or hub.city }} +

{{ hub.hub_blurb or 'A Walmart hub location.' }}

+ + + +
+ {% endfor %} +
+
+
+
+

{{ content.LOCATIONS_CLOSING }}

+
+
+
+

{{ content.FIND_ROLE_HEADING }}

+ +
+
+{% endblock %} diff --git a/sites/walmart_careers/templates/login.html b/sites/walmart_careers/templates/login.html new file mode 100644 index 00000000..e6c8e705 --- /dev/null +++ b/sites/walmart_careers/templates/login.html @@ -0,0 +1,25 @@ +{% extends "base_auth.html" %} +{% block title %}Sign in | Walmart Careers{% endblock %} +{% block content %} +
+

Sign in or create your account

+

Sign in with the email and password on your candidate account.

+ {% if errors %} + + {% endif %} +
+ + {% if next_url %}{% endif %} +
+ + +
+
+ + +
+ +
+

New here? Create a candidate account.

+
+{% endblock %} diff --git a/sites/walmart_careers/templates/register.html b/sites/walmart_careers/templates/register.html new file mode 100644 index 00000000..5e912450 --- /dev/null +++ b/sites/walmart_careers/templates/register.html @@ -0,0 +1,36 @@ +{% extends "base_auth.html" %} +{% block title %}Create an account | Walmart Careers{% endblock %} +{% block content %} +
+

Create your candidate account

+ {% if errors %} + + {% endif %} +
+ + {% if next_url %}{% endif %} +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+

Already have an account? Sign in.

+
+{% endblock %} diff --git a/sites/walmart_careers/templates/results.html b/sites/walmart_careers/templates/results.html new file mode 100644 index 00000000..f79fef80 --- /dev/null +++ b/sites/walmart_careers/templates/results.html @@ -0,0 +1,214 @@ +{% extends "base.html" %} +{% from "_job_card.html" import job_card %} +{% block title %}{{ total }} open roles | Walmart Careers{% endblock %} +{% block content %} +
+
+ + +
+ + + {% if filters.tab == 'future' %} +
+

Future roles

+

No future roles here

+

{{ content.EMPTY_FUTURE_ROLES }}

+ Back to open roles +
+ {% elif filters.tab == 'content' %} +
+

Content results

+

No content results

+

{{ content.EMPTY_CONTENT_TAB }}

+ Back to open roles +
+ {% else %} + +
+

{{ "{:,}".format(total) }} open role{{ '' if total == 1 else 's' }}

+
+ + + Add your location + +
+

Location

+ {% if filters.q %}{% endif %} + {% for key in ['area','category','brand','shift','type','rate'] %} + {% for value in filters[key] %}{% endfor %} + {% endfor %} + {% if filters.sort != 'relevance' %}{% endif %} +
+ + +
+ {% for radius in radius_values %} + + {% endfor %} +

A state or territory name (for example Puerto Rico or + PR) returns every role in that state and ignores the radius.

+
+ Reset + +
+
+
+
+ + {% if location_failed %} +

We couldn't find that location.

+ {% elif location %} +

+ {% if location.kind == 'state' %} + Showing roles in {{ location.label }} ({{ location.state }}). + {% else %} + Showing roles within {{ filters.radius }} miles of {{ location.label }}. + {% endif %} +

+ {% endif %} + +
+
+ + + Filters + +
+

Filters

+ {% if filters.q %}{% endif %} + {% if filters.loc %} + + + {% endif %} + {% if filters.sort != 'relevance' %}{% endif %} +
+
+ Brand + {% for value in brand_values %} + + {% endfor %} +
+
+ Shift + {% for value in shift_values %} + + {% endfor %} +
+
+ Employment Type + {% for value in employment_type_values %} + + {% endfor %} +
+
+ Rate + {% for value in rate_values %} + + {% endfor %} +
+
+ Career Area + {% for area in areas %} + +
+ Categories in {{ area.name }} + {% for category in area.categories %} + + {% endfor %} +
+ {% endfor %} +
+
+
+ Reset + +
+
+
+ +
+ Sort by: {{ 'Relevance' if filters.sort == 'relevance' else 'Most recent' }} + + +
+
+ + {% if filter_chips %} +
+ {% for chip in filter_chips %} + {{ chip.label }} × + {% endfor %} + Clear all +
+ {% endif %} + + {% if jobs %} +
+ {% for job in jobs %}{{ job_card(job, saved_ids) }}{% endfor %} +
+ {% if pages > 1 %} + + {% endif %} + {% else %} +
+

No roles matched

+

Try a different keyword, widen your location radius, or clear a filter.

+ Reset all filters +
+ {% endif %} + {% endif %} +
+
+
+{% endblock %} diff --git a/sites/walmart_careers/templates/saved_roles.html b/sites/walmart_careers/templates/saved_roles.html new file mode 100644 index 00000000..60298bc4 --- /dev/null +++ b/sites/walmart_careers/templates/saved_roles.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} +{% block body_class %}l1{% endblock %} +{% from "_job_card.html" import job_card %} +{% block title %}Saved roles | Walmart Careers{% endblock %} +{% block content %} +
+
+
+
+

{{ content.SAVED_PROMO_HEADLINE[0] }}
{{ content.SAVED_PROMO_HEADLINE[1] }}

+

{{ content.SAVED_PROMO_BLURB }}

+ {% if current_user.is_authenticated %} + See all open roles + {% else %} + Sign in or create account + {% endif %} +
+ A Walmart associate outside a store +
+
+
+ +
+
+

Saved roles ({{ rows|length }})

+ {% if not current_user.is_authenticated %} +

{{ content.SAVED_EMPTY_NOTE }} + Sign in to see the roles you have + saved, search for a role, or view your recommended roles below.

+ {% elif not rows %} +

{{ content.SAVED_EMPTY_NOTE }} + Search for a role, or view your recommended roles below.

+ {% else %} +
+ {% for row in rows %}{{ job_card(row.job, saved_ids, unsave=True) }}{% endfor %} +
+ {% endif %} +
+
+{# Recommendations only fill the page when there is nothing saved to show. #} +{% if not current_user.is_authenticated or not rows %} + +{% endif %} +{% endblock %} diff --git a/sites/walmart_careers/templates/terms.html b/sites/walmart_careers/templates/terms.html new file mode 100644 index 00000000..76bfad42 --- /dev/null +++ b/sites/walmart_careers/templates/terms.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} +{% block title %}Terms & Conditions | Walmart Careers{% endblock %} +{% block content %} +
+
+

{{ content.TERMS_HEADING }}

+ {% for heading, body in content.TERMS_SECTIONS %} +

{{ heading }}

+

{{ body }}

+ {% endfor %} + +
+
+{% endblock %} diff --git a/sites/walmart_careers/tests/test_app.py b/sites/walmart_careers/tests/test_app.py new file mode 100644 index 00000000..47fa2dde --- /dev/null +++ b/sites/walmart_careers/tests/test_app.py @@ -0,0 +1,320 @@ +from __future__ import annotations + +import os +import re +import shutil +import sys +from pathlib import Path + +import pytest + +SITE = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(SITE)) +os.environ["WEBSYN_SKIP_BOOTSTRAP"] = "1" + +import app as site # noqa: E402 +import seed_data # noqa: E402 + +SEED = SITE / "instance_seed" / "walmart_careers.db" + +if not SEED.exists(): + seed_data.build_seed_database() + + +@pytest.fixture(autouse=True) +def clean_database(): + with site.app.app_context(): + site.db.session.remove() + site.db.engine.dispose() + site.INSTANCE_DIR.mkdir(exist_ok=True) + shutil.copy2(SEED, site.DB_PATH) + site.app.config.update(TESTING=True, WTF_CSRF_ENABLED=False) + yield + with site.app.app_context(): + site.db.session.remove() + site.db.engine.dispose() + site.DB_PATH.unlink(missing_ok=True) + + +@pytest.fixture +def client(): + return site.app.test_client() + + +def login(client, email="alice.j@test.com"): + response = client.post("/login", data={"email": email, "password": "TestPass123!"}) + assert response.status_code == 302 + return response + + +def job_id(title: str, city: str) -> str: + with site.app.app_context(): + return ( + site.Job.query.join(site.Store) + .filter(site.Job.title == title, site.Store.city == city) + .one() + .job_id + ) + + +def test_security_configuration_and_malformed_session_identity(): + assert site.app.config["SECRET_KEY"] != "webharbor-walmart-careers-demo-key" + assert site.app.config["MAX_CONTENT_LENGTH"] == 256 * 1024 + assert site.app.config["SESSION_COOKIE_HTTPONLY"] is True + assert site.app.config["SESSION_COOKIE_SAMESITE"] == "Lax" + with site.app.app_context(): + assert site.load_user("not-an-integer") is None + + +def test_navigation_controller_is_packaged(client): + page = client.get("/") + assert page.status_code == 200 + assert b'/static/js/navigation.js' in page.data + script = client.get("/static/js/navigation.js") + assert script.status_code == 200 + assert b'addEventListener("pointerdown"' in script.data + assert b'addEventListener("keydown"' in script.data + assert b'closeMenus(menu)' in script.data + + +def test_health_and_seed_contract(client): + response = client.get("/_health") + assert response.status_code == 200 + assert response.get_json() == { + "ok": True, + "site": "walmart_careers", + "seed_version": "walmart-careers-v2", + "jobs": 246, + "stores": 51, + "areas": 7, + "categories": 33, + "users": 4, + } + with site.app.app_context(): + assert site.db.session.get(site.SeedMetadata, "version").value == site.SEED_VERSION + assert site.db.session.execute(site.db.text("PRAGMA foreign_key_check")).all() == [] + + +def test_all_public_pages_render(client): + paths = [ + "/", "/home", "/results", "/resources/location", + "/resources/hiring-process", "/resources/terms-and-conditions", "/about-us", + "/login", "/register", "/candidate-home/saved-roles", + ] + with site.app.app_context(): + paths += [f"/careers-areas/{row.slug}" for row in site.Area.query.all()] + paths += [f"/jobs/{row.job_id}" for row in site.Job.query.order_by(site.Job.job_id).limit(6)] + for path in paths: + response = client.get(path, follow_redirects=True) + assert response.status_code == 200, path + assert b"Traceback" not in response.data + + +@pytest.mark.parametrize("query", [ + "page=x", "page=0", "page=10001", "page=1&page=2", "radius=10", + "sort=unknown", "tab=unknown", "q=a&searchQuery=b", "q=x&q=y", + "brand=Target", "brand=Walmart&brand=Walmart", "shift=Night", + "type=Contract", "rate=Daily", "area=unknown", "category=unknown", +]) +def test_invalid_results_queries_fail_closed(client, query): + assert client.get(f"/results?{query}").status_code == 400 + + +def test_bounded_results_query_and_valid_filters(client): + assert client.get("/results?q=" + "x" * 161).status_code == 400 + assert client.get("/results?loc=" + "x" * 81).status_code == 400 + assert client.get("/results?q=" + "+".join(f"term{n}" for n in range(13))).status_code == 400 + response = client.get( + "/results?q=cashier&loc=Puerto+Rico&radius=25&shift=Weekday+Day&" + "type=Full+time&rate=Hourly&brand=Walmart&area=stores-and-clubs" + ) + assert response.status_code == 200 + assert b"open role" in response.data + + +def test_unknown_and_unseeded_locations_never_broaden_results(client): + for location in ("NoSuchPlace", "Alabama"): + response = client.get("/results", query_string={"loc": location}) + assert response.status_code == 200 + assert b"0 open roles" in response.data + assert b"No roles matched" in response.data + response = client.get("/results", query_string={"loc": "Puerto Rico", "radius": "25"}) + assert response.status_code == 200 + assert b"Showing roles in Puerto Rico (PR)" in response.data + + +def test_displayed_hashtag_returns_its_job_family(client): + response = client.get("/results", query_string={"q": "#freighthandlerjobs"}) + assert response.status_code == 200 + assert b"Freight Handler" in response.data + assert b"No roles matched" not in response.data + + +def test_safe_next_rejects_external_and_encoded_network_paths(client): + for target in ["https://example.com/", "//example.com/", "/%2f%2fexample.com/", "/\\example.com"]: + response = client.post( + "/login", data={"email": "alice.j@test.com", "password": "TestPass123!", "next": target} + ) + assert response.status_code == 302 + assert response.headers["Location"] == "/" + client.post("/logout") + response = client.post( + "/login", + data={"email": "alice.j@test.com", "password": "TestPass123!", "next": "/account?from=test"}, + ) + assert response.headers["Location"] == "/account?from=test" + + +def test_login_and_authenticated_ownership(client): + target = job_id("Yard Driver-Off Property", "Williamsburg") + response = client.post(f"/jobs/{target}/save") + assert response.status_code == 302 and response.headers["Location"].startswith("/login") + login(client) + assert client.post(f"/jobs/{target}/save").status_code == 302 + with site.app.app_context(): + alice = site.User.query.filter_by(email="alice.j@test.com").one() + bob = site.User.query.filter_by(email="bob.c@test.com").one() + assert site.SavedJob.query.filter_by(user_id=alice.id, job_id=target).count() == 1 + assert site.SavedJob.query.filter_by(user_id=bob.id, job_id=target).count() == 0 + assert client.post(f"/jobs/{target}/save").status_code == 302 + with site.app.app_context(): + alice = site.User.query.filter_by(email="alice.j@test.com").one() + assert site.SavedJob.query.filter_by(user_id=alice.id, job_id=target).count() == 1 + + +def test_runtime_state_remains_restartable_and_healthy(client): + login(client) + target = job_id("Yard Driver-Off Property", "Williamsburg") + client.post(f"/jobs/{target}/save") + client.post("/logout") + registration = {"first_name": "Runtime", "last_name": "User", "email": "runtime.user@example.com", + "password": "long-password", "confirm_password": "long-password"} + assert client.post("/register", data=registration).status_code == 302 + with site.app.app_context(): + seed_data.ensure_seed_database() + assert site.Job.query.count() == 246 + assert site.User.query.count() == 5 + assert site.SavedJob.query.count() == 14 + health = client.get("/_health") + assert health.status_code == 200 and health.get_json()["ok"] is True + + +def test_registration_validation_and_case_insensitive_uniqueness(client): + base = {"first_name": "Taylor", "last_name": "Reed", "password": "long-password", "confirm_password": "long-password"} + response = client.post("/register", data={**base, "email": "ALICE.J@TEST.COM"}) + assert response.status_code == 200 + assert b"already exists" in response.data + response = client.post("/register", data={**base, "email": "t@example.com", "first_name": "x" * 81}) + assert response.status_code == 200 + assert b"80 characters or fewer" in response.data + response = client.post("/register", data={**base, "email": "taylor.reed+pr86@example.com"}) + assert response.status_code == 302 + with site.app.app_context(): + assert site.User.query.filter_by(email="taylor.reed+pr86@example.com").count() == 1 + + +def test_profile_rejects_truncation_and_unbounded_values(client): + login(client, "david.k@test.com") + form = {"display_name": "David Kim", "first_name": "David", "last_name": "Kim", "phone": "214-555-0166", "city": "Rogers"} + response = client.post("/account/edit", data={**form, "state": "ARK"}) + assert response.status_code == 200 + assert b"2 characters or fewer" in response.data + response = client.post("/account/edit", data={**form, "state": "ZZ"}) + assert response.status_code == 200 + assert b"valid two-letter" in response.data + response = client.post("/account/edit", data={**form, "state": "AR", "city": "x" * 81}) + assert response.status_code == 200 + assert b"80 characters or fewer" in response.data + assert client.post("/account/edit", data={**form, "state": "AR"}).status_code == 302 + with site.app.app_context(): + david = site.User.query.filter_by(email="david.k@test.com").one() + assert (david.city, david.state) == ("Rogers", "AR") + + +def test_application_validation_and_submission(client): + login(client, "carol.d@test.com") + target = job_id("Pharmacy Technician", "Tacoma") + response = client.post( + f"/jobs/{target}/apply", + data={"email": "carol.d@test.com", "first_name": "Carol", "last_name": "Davis", + "phone": "1" * 16, "terms": "on"}, + ) + assert response.status_code == 200 + assert b"10 to 15 digits" in response.data + response = client.post( + f"/jobs/{target}/apply", + data={"email": "carol.d@test.com", "first_name": "Carol", "last_name": "Davis", + "phone": "253-555-0142", "terms": "on"}, + ) + assert response.status_code == 302 and response.headers["Location"].endswith("/apply/confirm") + response = client.post(response.headers["Location"]) + assert response.status_code == 302 and response.headers["Location"].endswith("/apply/submitted") + submitted = client.get(response.headers["Location"]) + assert submitted.status_code == 200 and b"WMC-000005" in submitted.data + with site.app.app_context(): + carol = site.User.query.filter_by(email="carol.d@test.com").one() + row = site.Application.query.filter_by(user_id=carol.id, job_id=target).one() + assert re.sub(r"\D", "", row.phone) == "2535550142" + + +def test_application_draft_cookie_is_opaque_and_replay_is_idempotent(client): + login(client, "carol.d@test.com") + target = job_id("Pharmacy Technician", "Tacoma") + response = client.post( + f"/jobs/{target}/apply", + data={"email": "carol.d@test.com", "first_name": "Carol", "last_name": "Davis", + "phone": "253-555-0142", "terms": "on"}, + ) + assert response.status_code == 302 + with client.session_transaction() as browser_session: + assert set(browser_session) <= {"_flashes", "_fresh", "_id", "_user_id", "apply_draft_token"} + assert "carol.d@test.com" not in repr(dict(browser_session)) + old_cookie = client.get_cookie("session").value + confirm = response.headers["Location"] + assert client.post(confirm).status_code == 302 + replay = site.app.test_client() + replay.set_cookie("session", old_cookie) + assert replay.post(confirm).status_code == 302 + with site.app.app_context(): + assert site.Application.query.filter_by(user_id=3, job_id=target).count() == 1 + assert site.ApplicationDraft.query.count() == 0 + + +def test_identity_change_invalidates_application_workflow(client): + login(client, "alice.j@test.com") + target = job_id("Pharmacy Technician", "Tacoma") + response = client.post( + f"/jobs/{target}/apply", + data={"email": "alice.j@test.com", "first_name": "Alice", "last_name": "Johnson", + "phone": "479-555-0134", "terms": "on"}, + ) + assert response.status_code == 302 + assert client.post("/login", data={"email": "bob.c@test.com", "password": "TestPass123!"}).status_code == 302 + assert client.get(response.headers["Location"]).headers["Location"].endswith(f"/jobs/{target}/apply") + with site.app.app_context(): + assert site.ApplicationDraft.query.count() == 0 + + +def test_submitted_application_requires_matching_owner(client): + login(client, "alice.j@test.com") + with client.session_transaction() as browser_session: + browser_session["apply_submitted_id"] = 2 + response = client.get("/jobs/CP-6014-11937/apply/submitted") + assert response.status_code == 302 + assert response.headers["Location"].endswith("/jobs/CP-6014-11937/apply") + + +def test_csrf_is_required_for_mutations(client): + site.app.config["WTF_CSRF_ENABLED"] = True + assert client.post("/login", data={"email": "alice.j@test.com", "password": "TestPass123!"}).status_code == 400 + page = client.get("/login") + token = re.search(rb'name="csrf_token"[^>]+value="([^"]+)"', page.data).group(1).decode() + response = client.post( + "/login", data={"csrf_token": token, "email": "alice.j@test.com", "password": "TestPass123!"} + ) + assert response.status_code == 302 + + +def test_request_body_limit(client): + response = client.post("/login", data={"email": "x" * (257 * 1024), "password": "x"}) + assert response.status_code == 413 diff --git a/sites/walmart_careers/tests/test_assets.py b/sites/walmart_careers/tests/test_assets.py new file mode 100644 index 00000000..0c39bd3c --- /dev/null +++ b/sites/walmart_careers/tests/test_assets.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +import hashlib +import json +import sqlite3 +import xml.etree.ElementTree as ET +from pathlib import Path +from urllib.parse import urlsplit + +from PIL import Image + +SITE = Path(__file__).resolve().parents[1] +MANIFEST = json.loads((SITE / "asset_inventory.json").read_text()) +TRACKED_MANIFEST = json.loads((SITE / "tracked_asset_inventory.json").read_text()) + + +def test_inventory_exactly_covers_runtime_images(): + rows = MANIFEST["assets"] + expected = {row["path"] for row in rows} + actual = { + str(path.relative_to(SITE)) + for path in (SITE / "static/images").iterdir() + if path.is_file() and path.name != ".gitkeep" + } + assert MANIFEST["schema_version"] == 1 + assert MANIFEST["asset_count"] == len(rows) == len(expected) == 35 + assert MANIFEST["total_bytes"] == sum(row["bytes"] for row in rows) == 11_509_127 + assert actual == expected + for row in rows: + path = SITE / row["path"] + data = path.read_bytes() + assert len(data) == row["bytes"] + assert hashlib.sha256(data).hexdigest() == row["sha256"] + source = urlsplit(row["source_url"]) + assert source.scheme == "https" and source.hostname + assert row["source_kind"] in {"direct_asset", "source_page"} + + +def test_all_runtime_images_fully_decode(): + for row in MANIFEST["assets"]: + path = SITE / row["path"] + with Image.open(path) as image: + image.load() + assert image.format in {"JPEG", "PNG"} + assert image.width > 0 and image.height > 0 + + +def test_database_and_content_image_references_exist(): + referenced = set() + connection = sqlite3.connect(SITE / "instance_seed/walmart_careers.db") + try: + for (value,) in connection.execute("SELECT hero_image FROM areas WHERE hero_image != ''"): + referenced.add(value) + for (value,) in connection.execute("SELECT hub_image FROM stores WHERE hub_image IS NOT NULL"): + referenced.add(value) + for (value,) in connection.execute("SELECT hero_images_json FROM jobs"): + referenced.update(json.loads(value)) + finally: + connection.close() + source_text = "\n".join( + path.read_text() + for path in [SITE / "_content.py", *sorted((SITE / "templates").glob("*.html"))] + ) + referenced.update(path.name for path in (SITE / "static/images").iterdir() if path.name in source_text) + available = {path.name for path in (SITE / "static/images").iterdir() if path.is_file()} + assert referenced <= available + + +def test_tracked_binary_inventory_is_exact(): + rows = TRACKED_MANIFEST["assets"] + expected = {row["path"] for row in rows} + actual = { + str(path.relative_to(SITE)) + for root in (SITE / "static/icons", SITE / "static/fonts") + for path in root.iterdir() + if path.is_file() and path.name != ".gitkeep" + } + assert TRACKED_MANIFEST["schema_version"] == 1 + assert TRACKED_MANIFEST["asset_count"] == len(rows) == len(expected) == 27 + assert actual == expected + for row in rows: + data = (SITE / row["path"]).read_bytes() + assert len(data) == row["bytes"] + assert hashlib.sha256(data).hexdigest() == row["sha256"] + source = urlsplit(row["source_url"]) + assert source.scheme == "https" and source.hostname + + +def test_tracked_svg_and_font_formats_are_valid(): + for path in (SITE / "static/icons").glob("*.svg"): + root = ET.fromstring(path.read_text()) + assert root.tag.endswith("svg") + assert not list(root.iter("script")) + assert (SITE / "static/icons/search_icon.png").read_bytes().startswith(b"\x89PNG\r\n\x1a\n") + assert (SITE / "static/fonts/EverydaySansUI-wght.ttf").read_bytes()[:4] == b"\x00\x01\x00\x00" diff --git a/sites/walmart_careers/tests/test_ground_truth.py b/sites/walmart_careers/tests/test_ground_truth.py new file mode 100644 index 00000000..4ba1a947 --- /dev/null +++ b/sites/walmart_careers/tests/test_ground_truth.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import sys +from pathlib import Path + +SITE = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(SITE / "verify")) + +from ground_truth import all_ground_truth # noqa: E402 + +SEED = SITE / "instance_seed" / "walmart_careers.db" +EXPECTED_TARGETS = { + 0: "CP-5991-12522", + 1: "R-2468347", + 2: "CP-9054-10921", + 3: "CP-5260-11531", + 4: "CP-4750-11130", + 5: "R-2411489", + 6: "CP-2073-11104", + 7: "R-2447168", + 8: "CP-1230-11592", + 9: "CP-6038-10642", + 10: "R-2456729", + 11: "CP-6088-10659", + 12: "CP-5991-11940", + 13: "CP-4137-10959", + 14: "CP-9046-11274", + 15: "R-2434655", + 16: "CP-2503-10981", + 17: "CP-2503-11505", + 18: "CP-6014-13829", + 19: "CP-1179-11202", +} + + +def test_all_tasks_have_unique_derived_ground_truth(): + facts = all_ground_truth(SEED) + assert set(facts) == set(range(20)) + assert {number: row["target"]["job_id"] for number, row in facts.items()} == EXPECTED_TARGETS + assert len(facts[8]["candidates"]) == 2 + assert len(facts[9]["candidates"]) == 2 + assert len(facts[10]["candidates"]) == 2 + assert len(facts[18]["candidates"]) == 2 + assert len(facts[16]["candidates"]) >= 2 + assert len(facts[19]["candidates"]) >= 2 + + +def test_stateful_task_preconditions_are_unique(): + facts = all_ground_truth(SEED) + assert len(facts[12]["candidates"]) == 1 + assert facts[12]["target"]["banner"] == "Neighborhood Market" + assert len(facts[17]["candidates"]) == 1 + assert facts[17]["target"]["employment_type"] == "Part time" + assert facts[15]["target"]["confirmation_no"] == "WMC-000004" diff --git a/sites/walmart_careers/tests/test_integration.py b/sites/walmart_careers/tests/test_integration.py new file mode 100644 index 00000000..1a6d9f44 --- /dev/null +++ b/sites/walmart_careers/tests/test_integration.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +import ast +import json +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[3] +SITE = ROOT / "sites/walmart_careers" +EXPECTED = [ + "allrecipes", "amazon", "apple", "arxiv", "bbc_news", "booking", "github", + "google_flights", "google_map", "google_search", "huggingface", "wolfram_alpha", + "cambridge_dictionary", "coursera", "espn", "merriam_webster", "ikea", "phys_org", + "target", "ted", "osu", "rotten_tomatoes", "compass", "walmart_careers", +] + + +def shell_sites(): + text = (ROOT / "websyn_start.sh").read_text() + return re.search(r"SITES=\((.*?)\)", text, re.S).group(1).split() + + +def control_sites(): + module = ast.parse((ROOT / "control_server.py").read_text()) + for node in module.body: + if isinstance(node, ast.Assign) and any(isinstance(target, ast.Name) and target.id == "SITES" for target in node.targets): + return ast.literal_eval(node.value) + raise AssertionError("control SITES not found") + + +def test_exact_24_site_registry_and_port(): + assert shell_sites() == control_sites() == EXPECTED + assert EXPECTED.index("rotten_tomatoes") + 40000 == 40021 + assert EXPECTED.index("compass") + 40000 == 40022 + assert EXPECTED.index("walmart_careers") + 40000 == 40023 + + +def test_docker_preserves_current_main_build_gates_and_adds_walmart(): + text = (ROOT / "Dockerfile").read_text() + assert "24 Flask mirror sites" in text + assert "EXPOSE 8101 40000-40023" in text + assert "check_asset_inventory.py /opt/WebSyn/compass" in text + assert "check_asset_inventory.py /opt/WebSyn/walmart_careers" in text + assert "walmart_careers/check_tracked_assets.py" in text + assert "cd /opt/WebSyn/compass" in text and "cd /opt/WebSyn/osu" in text + assert "cd /opt/WebSyn/rotten_tomatoes" in text and "cd /opt/WebSyn/walmart_careers" in text + + +def test_tasks_and_verifiers_are_complete_and_use_site_24(): + rows = [json.loads(line) for line in (SITE / "tasks.jsonl").read_text().splitlines() if line] + assert [row["id"] for row in rows] == [f"Walmart Careers--{number}" for number in range(20)] + assert {row["web"] for row in rows} == {"http://localhost:40023/"} + assert all((ROOT / row["verifier_path"]).is_file() for row in rows) + assert all("answer" not in row for row in rows) + assert all("A checkpoint passes only when the required evidence is present" in row["judge_rubric"] for row in rows) + + +def test_assets_pin_is_immutable_merged_revision(): + text = (ROOT / ".assets-revision").read_text() + revision = re.search(r"^revision:\s*([0-9a-f]+)$", text, re.M).group(1) + assert revision == "65c479f894763f64c6073e0d180ebf542d1d2c02" + assert (SITE / ".build-generated-seed").is_file() + assert (SITE / ".requires-images").is_file() + assert (SITE / "asset_inventory.json").is_file() + assert (SITE / "tracked_asset_inventory.json").is_file() + + +def test_shared_documentation_uses_24_site_range(): + for relative in ["README.md", "AGENTS.md", "CONTRIBUTING.md", "CLAUDE.md", "agent_demo/README.md"]: + text = (ROOT / relative).read_text() + assert "40000-40022" not in text, relative + assert "40000-40023" in text, relative + + +def test_no_merge_conflict_markers_in_release_files(): + for relative in ["README.md", "Dockerfile", "control_server.py", "websyn_start.sh"]: + text = (ROOT / relative).read_text() + assert not re.search(r"^(<<<<<<<|=======|>>>>>>>)", text, re.M), relative diff --git a/sites/walmart_careers/tests/test_seed_quality.py b/sites/walmart_careers/tests/test_seed_quality.py new file mode 100644 index 00000000..2a6e53af --- /dev/null +++ b/sites/walmart_careers/tests/test_seed_quality.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +import hashlib +import json +import os +import sqlite3 +import subprocess +import sys +from collections import Counter +from pathlib import Path + +SITE = Path(__file__).resolve().parents[1] +SEED = SITE / "instance_seed" / "walmart_careers.db" + + +def query(sql, params=()): + connection = sqlite3.connect(SEED) + connection.row_factory = sqlite3.Row + try: + return [dict(row) for row in connection.execute(sql, params)] + finally: + connection.close() + + +def test_seed_schema_counts_marker_and_foreign_keys(): + connection = sqlite3.connect(SEED) + try: + tables = {row[0] for row in connection.execute("SELECT name FROM sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%'")} + assert tables == {"application_drafts", "applications", "areas", "categories", "jobs", "saved_jobs", "seed_metadata", "stores", "users"} + assert connection.execute("SELECT value FROM seed_metadata WHERE key='version'").fetchone() == ("walmart-careers-v2",) + assert connection.execute("PRAGMA foreign_key_check").fetchall() == [] + expected = {"areas": 7, "categories": 33, "stores": 51, "jobs": 246, "users": 4, "saved_jobs": 13, "applications": 4, "application_drafts": 0} + assert {table: connection.execute(f"SELECT count(*) FROM {table}").fetchone()[0] for table in expected} == expected + finally: + connection.close() + + +def test_catalog_cross_field_invariants(): + rows = query( + "SELECT j.*, a.slug area_slug, c.area_id category_area_id, s.state, s.lat, s.lng " + "FROM jobs j JOIN areas a ON a.id=j.area_id JOIN categories c ON c.id=j.category_id JOIN stores s ON s.id=j.store_id" + ) + assert len({row["job_id"] for row in rows}) == 246 + assert all(row["category_area_id"] == row["area_id"] for row in rows) + assert all(row["population"] in {"hourly", "salaried"} for row in rows) + assert all((row["population"] == "hourly") == (row["pay_frequency"] == "Hourly") for row in rows) + assert all(float(row["min_pay"]) <= float(row["max_pay"]) for row in rows) + assert all(-90 <= row["lat"] <= 90 and -180 <= row["lng"] <= 180 for row in rows) + assert all((row["positions_available"] is not None) == (row["population"] == "hourly") for row in rows) + assert all((row["worker_type"] is not None) == (row["population"] == "salaried") for row in rows) + assert all((row["employment_type"] == "Intern") <= (row["area_slug"] == "students") for row in rows) + distributions = { + "population": Counter(row["population"] for row in rows), + "brand": Counter(row["brand"] for row in rows), + } + assert distributions["population"] == {"hourly": 168, "salaried": 78} + assert distributions["brand"] == {"Walmart": 184, "Sam's Club": 42, "Vizio": 20} + shift_counts = Counter(shift for row in rows for shift in json.loads(row["shifts_json"])) + assert len(shift_counts) == 7 and min(shift_counts.values()) >= 25 + + +def test_seed_rebuild_is_byte_identical_across_process_hash_seeds(tmp_path): + hashes = [] + for hash_seed in ("0", "1", "0"): + environment = {**os.environ, "PYTHONHASHSEED": hash_seed, "WEBSYN_SKIP_BOOTSTRAP": "1"} + subprocess.run([sys.executable, str(SITE / "seed_data.py")], cwd=SITE, env=environment, check=True, capture_output=True, text=True) + hashes.append(hashlib.sha256(SEED.read_bytes()).hexdigest()) + assert len(set(hashes)) == 1 + + +def test_partial_or_unversioned_database_fails_closed(tmp_path): + script = f""" +import os, shutil, sys +sys.path.insert(0, {str(SITE)!r}) +os.environ['WEBSYN_SKIP_BOOTSTRAP']='1' +import app +from seed_data import ensure_seed_database +with app.app.app_context(): + app.db.drop_all(); app.db.create_all() + app.db.session.add(app.Area(slug='partial', name='Partial', display_order=0)) + app.db.session.commit() + try: + ensure_seed_database() + except RuntimeError as exc: + assert 'partial' in str(exc) + else: + raise AssertionError('partial database was accepted') +""" + result = subprocess.run([sys.executable, "-c", script], cwd=tmp_path, env={**os.environ, "WEBSYN_SKIP_BOOTSTRAP": "1"}, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + subprocess.run([sys.executable, str(SITE / "seed_data.py")], cwd=SITE, env={**os.environ, "PYTHONHASHSEED": "0"}, check=True, capture_output=True) diff --git a/sites/walmart_careers/tracked_asset_inventory.json b/sites/walmart_careers/tracked_asset_inventory.json new file mode 100644 index 00000000..bff8c00e --- /dev/null +++ b/sites/walmart_careers/tracked_asset_inventory.json @@ -0,0 +1,196 @@ +{ + "schema_version": 1, + "asset_count": 27, + "total_bytes": 250542, + "assets": [ + { + "path": "static/icons/benefit-financial.svg", + "bytes": 6383, + "sha256": "2fc4fea17ecfae3ffd9e9041840f2e7eedb388c744f3e60bb3672f3ec99c447c", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/icons/home/Financial%20perks.svg", + "source_kind": "direct_asset" + }, + { + "path": "static/icons/benefit-growth.svg", + "bytes": 2876, + "sha256": "2f9fb25bda585a41c2f9325f305ec87b32cca5e33ed692b82783e88bada79c4f", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/icons/home/Career%20growth%20opportunitiees.svg", + "source_kind": "direct_asset" + }, + { + "path": "static/icons/benefit-health.svg", + "bytes": 3468, + "sha256": "e22b76df784a3989e7c86a57736ca0ac2cea0671ee66a4cc9d511f26ca0c4af1", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/icons/home/Comprehensive%20health%20benefits.svg", + "source_kind": "direct_asset" + }, + { + "path": "static/icons/benefit-pto.svg", + "bytes": 4616, + "sha256": "4c1d2179480977508b7c05df99bdcd4bbda16e089842d93280d04041c063f1a3", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/icons/home/Paid%20time%20off.svg", + "source_kind": "direct_asset" + }, + { + "path": "static/icons/benefit-wellbeing.svg", + "bytes": 6046, + "sha256": "ff8ce3532bf1f22fbf33e04052e6bf28b97f3dcd1b1a5c33aae49a00ad4f75fa", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/icons/home/Wellbeing%20programs.svg", + "source_kind": "direct_asset" + }, + { + "path": "static/icons/header-mobile-logo.svg", + "bytes": 6007, + "sha256": "e284cca4d0cbc01909de43b8f787a70e6ea19bf9ece11781f91f8f2b6a871eec", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/global/header-mobile-logo.svg", + "source_kind": "direct_asset" + }, + { + "path": "static/icons/heart-blue.svg", + "bytes": 22243, + "sha256": "9366323fd6cbd56ed59403a9cdb1420adee5e6a2ee90058738f52a8b93a23c41", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/home/heart-blue.svg", + "source_kind": "direct_asset" + }, + { + "path": "static/icons/home-logo.svg", + "bytes": 5977, + "sha256": "d9836315ea6c1c28601ea82e2440564d8d0f844020e9e9814de76eb19917a969", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/global/home-logo.svg", + "source_kind": "direct_asset" + }, + { + "path": "static/icons/sams-club-text.svg", + "bytes": 4164, + "sha256": "6aab91011c72882631b0993168a67460b17529d9ee621c08bd9090fa9169de84", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/global/sams-clud-text.svg", + "source_kind": "direct_asset" + }, + { + "path": "static/icons/sams-logo.svg", + "bytes": 1054, + "sha256": "8e869f60baf529a64824d512dd521ef41b8a00b2b655a637a0e4ea54df49a598", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/global/sams-logo.svg", + "source_kind": "direct_asset" + }, + { + "path": "static/icons/sams-spark.svg", + "bytes": 1101, + "sha256": "7587b9260767a931531972ee625992d609ebf0fe6795627602e96420ef32a053", + "source_url": "https://careers.walmart.com/us/en/home", + "source_kind": "source_page" + }, + { + "path": "static/icons/search_icon.png", + "bytes": 2414, + "sha256": "7b4b39ed258d693766ee619bb767eec40049b4de95ab453d617c19cf8724a952", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/global/search_icon.png", + "source_kind": "direct_asset" + }, + { + "path": "static/icons/social-facebook.svg", + "bytes": 446, + "sha256": "2c7d09ef3fe7896e01ddbf13726dab8bccedac3a05ef425565032a9af38c3eed", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/global/social-icons/Facebook.svg", + "source_kind": "direct_asset" + }, + { + "path": "static/icons/social-glassdoor.svg", + "bytes": 656, + "sha256": "05f378f78c3d5077e2242c4363ecf5d75bbcf74957ace94a660329c028140f43", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/global/social-icons/Glassdoor.svg", + "source_kind": "direct_asset" + }, + { + "path": "static/icons/social-instagram.svg", + "bytes": 900, + "sha256": "ec96fc3b19f7b841878361b02651676a989d2d23505b88ddd9031b9275b5e355", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/global/social-icons/Instagram.svg", + "source_kind": "direct_asset" + }, + { + "path": "static/icons/social-linkedin.svg", + "bytes": 883, + "sha256": "4339e956682a37daa18b48c94f543bc6dc57e67b5b2c99f90c4f1a8ad883accf", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/global/social-icons/LinkedIn.svg", + "source_kind": "direct_asset" + }, + { + "path": "static/icons/social-x.svg", + "bytes": 322, + "sha256": "ef0c0c12db88d35009344fcfa7b5159f5eb0cf262417dd48b4b021213bb6fc18", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/global/social-icons/XApp.svg", + "source_kind": "direct_asset" + }, + { + "path": "static/icons/social-youtube.svg", + "bytes": 964, + "sha256": "a15676e2e72c0dc0cb6b479a98e1ad1034989180e8619ae9abc6b7d342393e09", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/global/social-icons/YouTube.svg", + "source_kind": "direct_asset" + }, + { + "path": "static/icons/spark-white.svg", + "bytes": 2037, + "sha256": "e343e974f84df9d16f461cf922967016b0528c8ca8452fa4a1571af3febf98e9", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/icons/home/spark-white.svg", + "source_kind": "direct_asset" + }, + { + "path": "static/icons/spark-yellow-card.svg", + "bytes": 2020, + "sha256": "d256212acfdc8d6ab3b7f3b66f711c27a01724eae843a9dfd2f53dd7a2044320", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/home/WalmartSparkYellow.svg", + "source_kind": "direct_asset" + }, + { + "path": "static/icons/spark-yellow.svg", + "bytes": 2049, + "sha256": "a00efd3585fe55e7de48f5ab3657fe02e0967e0c64feebc0db2d6eaee7652792", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/icons/home/spark-yellow.svg", + "source_kind": "direct_asset" + }, + { + "path": "static/icons/spark.svg", + "bytes": 2040, + "sha256": "d32148ff56ddbc5085b438815efc0d87977478f5ee2def48628c3e751b21ab03", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/home/spark.svg", + "source_kind": "direct_asset" + }, + { + "path": "static/icons/tile-card.svg", + "bytes": 614, + "sha256": "9bd666f5c029276f3bbdc4394041ba382940d76db70972a87824e0d5ec19d374", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/icons/job-details/icon-card-white.svg", + "source_kind": "direct_asset" + }, + { + "path": "static/icons/tile-graduation.svg", + "bytes": 1313, + "sha256": "c037e095ec6234a5f69ecc0c3f8eaec3b8e838fa845835220d3e37d4f4af35ac", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/icons/job-details/graduation-blue.svg", + "source_kind": "direct_asset" + }, + { + "path": "static/icons/tile-growth.svg", + "bytes": 1155, + "sha256": "b3603d03b32bc05e10542c15e7787bd791897c215b5fc6b427b729e01114912a", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/icons/job-details/career_growth_white.svg", + "source_kind": "direct_asset" + }, + { + "path": "static/icons/tile-walmart-plus.svg", + "bytes": 2326, + "sha256": "4e3d0d25ad7b7e118cc14097f65db3dd81c1242af9f0b488345a4cd56dc5faed", + "source_url": "https://cms.careers.walmart.com/content/dam/careers/icons/job-details/walmart-plus-white.svg", + "source_kind": "direct_asset" + }, + { + "path": "static/fonts/EverydaySansUI-wght.ttf", + "bytes": 166468, + "sha256": "1bd7cae8dd0528cb4f5bdf7cac60e319dacc7ec0411104039e0ad3c7330f6f1c", + "source_url": "https://i5.walmartimages.com/dfw/99530ed8-2a92/5b784fd4-8106-49e0-aebe-2dbb02dc80c8/v2/_next/static/media/EverydaySansUI-wght.df2631f7.ttf", + "source_kind": "direct_asset" + } + ] +} diff --git a/sites/walmart_careers/verify/README.md b/sites/walmart_careers/verify/README.md new file mode 100644 index 00000000..ae5d7596 --- /dev/null +++ b/sites/walmart_careers/verify/README.md @@ -0,0 +1,50 @@ +# Walmart Careers deterministic grading contract + +Each row in `sites/walmart_careers/tasks.jsonl` points to `verify_0.py` through `verify_19.py`. The wrappers use `verify_lib.py` for package, URL, answer and state validation and `ground_truth.py` to derive every qualifying set and target from the supplied initial SQLite snapshot. No verifier calls an LLM. + +## Inputs + +```bash +python sites/walmart_careers/verify/verify_0.py \ + --run_dir /absolute/path/to/run \ + --initial_db /absolute/path/to/initial.db \ + --after_db /absolute/path/to/after.db +``` + +If explicit snapshots are omitted, the verifier checks `/initial.db` and `/after.db`, then falls back to `docker cp` from `$WH_CONTAINER` or `wh-review`. Missing or invalid inputs fail closed. Output is JSON with `task_id`, `pass`, `reason`, and `evidence`; exit code 0 means PASS and 1 means FAIL. + +## Package validation + +Every run must provide: + +- the exact task ID; +- a nonempty final answer; +- `terminated: true` with `termination_reason: agent_done`; +- at least one recorded step; +- HTTP URLs on the same loopback origin and port as `start_url`; +- both referenced screenshots for every step; +- PNG files that fully decode to nonempty images. + +The verifier validates recorder packaging and declared browser history. A deterministic verifier cannot cryptographically authenticate the recorder or bind screenshot pixels to the declared URL, so release evidence also includes independent Playwright execution against the packaged image. + +## Snapshot validation + +The initial and after snapshots must have the exact nine-table Walmart Careers schema, including `seed_metadata` version `walmart-careers-v2`. The initial snapshot must contain 246 jobs, 51 stores, 33 categories, seven areas, four users and no application drafts. `areas`, `categories`, `stores`, `jobs`, `seed_metadata`, and `application_drafts` must be row-identical before and after. Any schema change or immutable-catalog mutation fails closed. + +Read-only tasks additionally preserve `users`, `saved_jobs`, and `applications`. Stateful verifiers enforce exact added, removed or changed rows across all mutable tables. They reject collateral writes, stale-state no-ops, duplicate logical actions, changes to existing applications, extra users and extra saves. + +## Ground truth and workflows + +`ground_truth.py` queries the initial snapshot and fails on missing candidates, unexpected candidate cardinality, or tied extrema. Comparison tasks require every detail page needed to read hidden comparison values. Tasks requiring login, registration, career-area navigation, filters, application confirmation, profile update or save/remove actions enforce the required page order. + +Results gates parse query parameters. Facets use exact values, scalar values require exact equality, locations require the intended city/state scope, and keyword searches use whole normalized tokens. Task-requested filters must appear together when the task requires their conjunction. + +Answer matchers require affirmative values and reject negated occurrences. Requisition IDs, confirmation numbers, streets, shift windows, position counts, worker types, hashtags and qualification text are checked against the dynamically validated target. Complete street directionals and suffixes are required, with standard abbreviations accepted. + +## Tests + +```bash +python -m pytest sites/walmart_careers/tests sites/walmart_careers/verify/tests -q +``` + +The verifier fixtures copy the complete generated seed schema and invoke every verifier as a subprocess. Regression coverage includes malformed package evidence, corrupt PNGs, wrong task IDs, wrong answers, missing filters/pages, reordered workflows, schema/catalog mutation, unrelated state writes, stale-state preconditions, duplicate or extra rows, and positive representation variants. Application tests separately exercise the actual Flask routes and generated seed. diff --git a/sites/walmart_careers/verify/ground_truth.py b/sites/walmart_careers/verify/ground_truth.py new file mode 100644 index 00000000..0aff9f82 --- /dev/null +++ b/sites/walmart_careers/verify/ground_truth.py @@ -0,0 +1,213 @@ +"""Derive Walmart Careers task targets from a supplied initial SQLite snapshot.""" +from __future__ import annotations + +import json +import re +import sqlite3 +from pathlib import Path +from typing import Any, Callable + + +def _rows(db_path: str | Path) -> list[dict[str, Any]]: + connection = sqlite3.connect(str(db_path)) + connection.row_factory = sqlite3.Row + try: + rows = connection.execute( + "SELECT j.*, s.store_number, s.banner, s.location_name, s.street, s.city, s.state, s.zip, s.lat, s.lng, " + "a.slug AS area_slug, a.name AS area_name, c.slug AS category_slug, c.name AS category_name " + "FROM jobs j JOIN stores s ON s.id=j.store_id JOIN areas a ON a.id=j.area_id " + "JOIN categories c ON c.id=j.category_id ORDER BY j.job_id" + ).fetchall() + output = [dict(row) for row in rows] + finally: + connection.close() + for row in output: + row["shifts"] = json.loads(row.get("shifts_json") or "[]") + row["minimum_qualifications"] = json.loads(row.get("min_qualifications_json") or "[]") + return output + + +def _one(rows: list[dict[str, Any]], description: str) -> dict[str, Any]: + if len(rows) != 1: + raise ValueError(f"{description} must be unique; observed {len(rows)} rows: {[row.get('job_id') for row in rows]}") + return rows[0] + + +def _extreme(rows: list[dict[str, Any]], key: Callable[[dict[str, Any]], Any], maximum: bool, description: str) -> dict[str, Any]: + if not rows: + raise ValueError(f"{description} has no candidates") + value = (max if maximum else min)(key(row) for row in rows) + winners = [row for row in rows if key(row) == value] + return _one(winners, description) + + +def _clock_minutes(window: str) -> int: + match = re.search(r"\b(\d{1,2})(?::(\d{2}))?\s*([ap])\.?m", window or "", re.I) + if not match: + raise ValueError(f"cannot parse shift window {window!r}") + hour = int(match.group(1)) % 12 + (12 if match.group(3).lower() == "p" else 0) + return hour * 60 + int(match.group(2) or 0) + + +def _option_years(row: dict[str, Any], index: int = 1) -> int: + options = row["minimum_qualifications"] + if len(options) <= index: + raise ValueError(f"missing qualification option {index + 1} for {row['job_id']}") + values = [int(value) for value in re.findall(r"\b(\d+)\s+years?\b", options[index], re.I)] + if len(values) != 1: + raise ValueError(f"qualification years are not unique for {row['job_id']}: {values}") + return values[0] + + +def _user_id(connection: sqlite3.Connection, email: str) -> int: + row = connection.execute("SELECT id FROM users WHERE lower(email)=lower(?)", (email,)).fetchone() + if row is None: + raise ValueError(f"missing benchmark user {email}") + return int(row[0]) + + +def _saved_rows(db_path: str | Path, jobs: list[dict[str, Any]], email: str) -> list[dict[str, Any]]: + by_id = {row["job_id"]: row for row in jobs} + connection = sqlite3.connect(str(db_path)) + try: + user_id = _user_id(connection, email) + ids = [row[0] for row in connection.execute("SELECT job_id FROM saved_jobs WHERE user_id=? ORDER BY id", (user_id,))] + finally: + connection.close() + try: + return [by_id[job_id] for job_id in ids] + except KeyError as exc: + raise ValueError(f"saved role references unknown job {exc.args[0]}") from exc + + +def task_ground_truth(db_path: str | Path, task_number: int) -> dict[str, Any]: + jobs = _rows(db_path) + select = lambda predicate: [row for row in jobs if predicate(row)] + + if task_number == 0: + candidates = select(lambda row: row["title"] == "Optician" and row["banner"] == "Neighborhood Market" and row["city"] == "Wichita" and row["state"] == "KS") + target = _one(candidates, "task 0 Wichita Neighborhood Market Optician") + elif task_number == 1: + candidates = select(lambda row: row["title"].startswith("Staff, Software Engineer") and row["city"] == "Sunnyvale" and row["state"] == "CA") + target = _one(candidates, "task 1 Sunnyvale Staff Software Engineer") + elif task_number == 2: + candidates = select(lambda row: row["title"] == "Freight Handler" and row["store_number"] == "9054" and row["city"] == "Porterville" and row["state"] == "CA") + target = _one(candidates, "task 2 Porterville Freight Handler") + elif task_number == 3: + candidates = select(lambda row: row["title"] == "Pharmacy Technician" and row["area_slug"] == "healthcare" and row["city"] == "Bentonville" and row["state"] == "AR") + target = _one(candidates, "task 3 Bentonville Pharmacy Technician") + elif task_number == 4: + candidates = select(lambda row: row["brand"] == "Sam's Club" and row["employment_type"] == "Part time" and "Weekend Overnight" in row["shifts"] and float(row["max_pay"]) <= 20 and row["state"] == "TX") + target = _one(candidates, "task 4 qualifying Texas role") + elif task_number == 5: + candidates = select(lambda row: row["employment_type"] == "Full time" and row["area_slug"] == "technology" and row["city"] == "Hoboken" and row["state"] == "NJ" and float(row["max_pay"]) > 200000) + target = _one(candidates, "task 5 qualifying Hoboken Technology role") + elif task_number == 6: + candidates = select(lambda row: row["title"] == "Online Order Filling Team Supervisor" and row["city"] == "Cleveland" and row["state"] == "OH" and row["employment_type"] == "Full time" and "Weekday Day" in row["shifts"]) + target = _one(candidates, "task 6 Cleveland supervisor") + elif task_number == 7: + candidates = select(lambda row: row["area_slug"] == "students" and row["employment_type"] == "Intern" and row["brand"] == "Sam's Club" and "merchandising" in row["title"].lower() and row["city"] == "Bentonville" and row["state"] == "AR") + target = _one(candidates, "task 7 merchandising internship") + elif task_number == 8: + candidates = select(lambda row: row["title"] == "Auto Care Center Technician" and row["state"] == "MS") + if len(candidates) != 2: + raise ValueError(f"task 8 requires two candidates; observed {len(candidates)}") + target = _extreme(candidates, lambda row: int(row["positions_available"]), True, "task 8 maximum positions") + elif task_number == 9: + candidates = select(lambda row: row["title"] == "Freight Handler" and row["city"] == "Marcy" and row["state"] == "NY") + if len(candidates) != 2: + raise ValueError(f"task 9 requires two candidates; observed {len(candidates)}") + target = _extreme(candidates, lambda row: _clock_minutes(row["shift_time"]), False, "task 9 earliest shift") + elif task_number == 10: + title = "Senior Manager, Delivery Search, Arrival & Matching (Last Mile Delivery)" + candidates = select(lambda row: row["title"] == title and (row["city"], row["state"]) in {("Bentonville", "AR"), ("Hoboken", "NJ")}) + if len(candidates) != 2: + raise ValueError(f"task 10 requires two candidates; observed {len(candidates)}") + target = _extreme(candidates, _option_years, True, "task 10 maximum Option 2 experience") + elif task_number == 11: + candidates = select(lambda row: "Yard Driver" in row["title"] and row["city"] == "Williamsburg" and row["state"] == "VA") + target = _one(candidates, "task 11 Williamsburg Yard Driver") + elif task_number == 12: + candidates = [row for row in _saved_rows(db_path, jobs, "bob.c@test.com") if row["banner"] == "Neighborhood Market"] + target = _one(candidates, "task 12 Bob Neighborhood Market saved role") + elif task_number == 13: + candidates = select(lambda row: row["title"] == "Pharmacy Technician" and row["city"] == "Tacoma" and row["state"] == "WA") + target = _one(candidates, "task 13 Tacoma Pharmacy Technician") + elif task_number == 14: + candidates = select(lambda row: row["title"] == "eCom Warehouse Worker" and row["store_number"] == "9046" and row["city"] == "Marcy" and row["state"] == "NY") + target = _one(candidates, "task 14 Marcy eCom Warehouse Worker") + elif task_number == 15: + connection = sqlite3.connect(str(db_path)); connection.row_factory = sqlite3.Row + try: + user_id = _user_id(connection, "david.k@test.com") + rows = [dict(row) for row in connection.execute("SELECT * FROM applications WHERE user_id=? ORDER BY id", (user_id,))] + finally: + connection.close() + target = _one(rows, "task 15 David existing application") + candidates = rows + elif task_number == 16: + candidates = select(lambda row: row["state"] == "PR" and row["population"] == "hourly" and "Weekday Day" in row["shifts"] and "Cashier" in row["title"] and row["positions_available"] is not None) + target = _extreme(candidates, lambda row: int(row["positions_available"]), True, "task 16 maximum PR cashier positions") + elif task_number == 17: + candidates = [row for row in _saved_rows(db_path, jobs, "alice.j@test.com") if row["employment_type"] == "Part time"] + target = _one(candidates, "task 17 Alice Part time saved role") + elif task_number == 18: + candidates = select(lambda row: row["title"] == "Class A CDL Truck Driver" and (row["city"], row["state"]) in {("Ottawa", "KS"), ("Williamsburg", "VA")}) + if len(candidates) != 2: + raise ValueError(f"task 18 requires two candidates; observed {len(candidates)}") + target = _extreme(candidates, lambda row: int(row["positions_available"]), True, "task 18 maximum positions") + elif task_number == 19: + candidates = select(lambda row: row["area_slug"] == "stores-and-clubs" and row["category_slug"] == "digital-pickup-and-delivery" and row["employment_type"] == "Full time" and row["positions_available"] is not None) + target = _extreme(candidates, lambda row: int(row["positions_available"]), False, "task 19 minimum positions") + else: + raise ValueError(f"unsupported Walmart Careers task {task_number}") + return {"task": task_number, "target": target, "candidates": candidates} + + +def _shift_endpoints(window: str) -> tuple[str, str]: + values = re.findall(r"\b\d{1,2}(?::\d{2})?\s*[ap]\.?m", window or "", re.I) + if len(values) != 2: + raise ValueError(f"shift window does not have two endpoints: {window!r}") + return values[0], values[1] + + +def constants_for_task(db_path: str | Path, task_number: int) -> dict[str, Any]: + fact = task_ground_truth(db_path, task_number) + target, candidates = fact["target"], fact["candidates"] + values: dict[str, Any] = {"JOB_ID": target["job_id"]} + if task_number in {0, 6, 7, 19}: + values["STREET"] = target["street"] + if task_number == 1: + option = target["minimum_qualifications"][1] + values.update(OPTION_2_EXACT=re.sub(r"^Option\s*2:\s*", "", option, flags=re.I), OPTION_1_EXACT=target["minimum_qualifications"][0]) + if task_number in {2, 9, 17, 18}: + values["SHIFT_START"], values["SHIFT_END"] = _shift_endpoints(target["shift_time"]) + if task_number in {2, 3, 4, 6, 8, 16, 18}: + values["POSITIONS"] = int(target["positions_available"]) + if task_number == 3: + values["HASHTAG"] = target["hashtag"] + if task_number == 5: + values["DEGREE"] = "bachelor's degree in computer science" + if task_number == 7: + values["WORKER_TYPE_FRAGMENTS"] = tuple(part.strip().casefold() for part in re.split(r"[()/]", target["worker_type"]) if part.strip()) + if task_number in {8, 9, 10, 18}: + loser = _one([row for row in candidates if row["job_id"] != target["job_id"]], f"task {task_number} losing candidate") + values["LOSER_ID"] = loser["job_id"] + if task_number == 8: + values.update(WINNER_ID=target["job_id"], WINNER_STORE=int(target["store_number"]), LOSER_STORE=int(loser["store_number"])) + if task_number == 10: + values["YEARS"] = _option_years(target) + if task_number == 15: + values["CONFIRMATION_NO"] = target["confirmation_no"] + if task_number in {13, 17}: + email = "carol.d@test.com" if task_number == 13 else "alice.j@test.com" + connection = sqlite3.connect(str(db_path)) + try: + values["USER_ID"] = _user_id(connection, email) + finally: + connection.close() + return values + + +def all_ground_truth(db_path: str | Path) -> dict[int, dict[str, Any]]: + return {number: task_ground_truth(db_path, number) for number in range(20)} diff --git a/sites/walmart_careers/verify/tests/_support.py b/sites/walmart_careers/verify/tests/_support.py new file mode 100644 index 00000000..c7a99970 --- /dev/null +++ b/sites/walmart_careers/verify/tests/_support.py @@ -0,0 +1,271 @@ +"""Shared fixtures for the walmart_careers verifier tests. + +Synthetic SQLite snapshots (same table shapes as instance_seed/walmart_careers.db, +only the columns the verifiers read are populated) and a hand-written trajectory +writer in the agent_demo/agent.py format. No docker, no LLM. +""" +from __future__ import annotations + +import base64 +import copy +import json +import shutil +import sqlite3 +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path +from typing import Any + +VERIFY_DIR = Path(__file__).resolve().parents[1] +BASE = "http://localhost:41023" +PASSWORD = "TestPass123!" +SITE_DIR = VERIFY_DIR.parent +SEED_DB = SITE_DIR / "instance_seed" / "walmart_careers.db" +if not SEED_DB.exists(): + subprocess.run([sys.executable, str(SITE_DIR / "seed_data.py")], cwd=SITE_DIR, check=True) + +SCHEMA = """ +CREATE TABLE users ( + id INTEGER PRIMARY KEY, email TEXT NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE, + display_name TEXT NOT NULL DEFAULT '', first_name TEXT NOT NULL DEFAULT '', + last_name TEXT NOT NULL DEFAULT '', phone TEXT NOT NULL DEFAULT '', + city TEXT NOT NULL DEFAULT '', state TEXT NOT NULL DEFAULT '', + password_hash TEXT NOT NULL DEFAULT 'x', created_at TEXT NOT NULL DEFAULT '2026-08-01 00:00:00' +); +CREATE TABLE stores (id INTEGER PRIMARY KEY, store_number TEXT NOT NULL, street TEXT NOT NULL DEFAULT ''); +CREATE TABLE jobs (job_id TEXT PRIMARY KEY, title TEXT NOT NULL DEFAULT '', store_id INTEGER NOT NULL DEFAULT 1); +CREATE TABLE saved_jobs ( + id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL, job_id TEXT NOT NULL, + saved_at TEXT NOT NULL DEFAULT '2026-08-01 00:00:00', UNIQUE (user_id, job_id) +); +CREATE TABLE applications ( + id INTEGER PRIMARY KEY, job_id TEXT NOT NULL, user_id INTEGER, email TEXT NOT NULL, + first_name TEXT NOT NULL DEFAULT '', last_name TEXT NOT NULL DEFAULT '', phone TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'Submitted', confirmation_no TEXT NOT NULL UNIQUE, + submitted_at TEXT NOT NULL DEFAULT '2026-08-01 00:00:00' +); +""" + +# Same ids/emails/relations as the frozen seed (values are synthetic). +SEED_USERS = [ + dict(id=1, email="alice.j@test.com", username="alice.j", first_name="Alice", last_name="Johnson", + phone="479-555-0134", city="Bentonville", state="AR"), + dict(id=2, email="bob.c@test.com", username="bob.c", first_name="Bob", last_name="Chen", + phone="206-555-0178", city="Seattle", state="WA"), + dict(id=3, email="carol.d@test.com", username="carol.d", first_name="Carol", last_name="Davis", + phone="253-555-0119", city="Tacoma", state="WA"), + dict(id=4, email="david.k@test.com", username="david.k", first_name="David", last_name="Kim", + phone="214-555-0166", city="Dallas", state="TX"), +] +SEED_SAVED = [ + (1, 1, "CP-9046-10913"), (2, 1, "CP-2503-11505"), (3, 1, "CP-5991-12522"), + (4, 1, "CP-6088-12101"), (5, 1, "R-2423457"), (6, 1, "CP-4137-10533"), + (7, 2, "CP-5991-11940"), (8, 2, "CP-6038-12357"), (9, 2, "R-2420377"), + (10, 3, "CP-4137-10959"), (11, 3, "CP-6318-12289"), + (12, 4, "CP-4750-11130"), (13, 4, "R-2429348"), +] +SEED_APPLICATIONS = [ + dict(id=1, job_id="CP-144-11515", user_id=1, email="alice.j@test.com", phone="479-555-0134", confirmation_no="WMC-000001"), + dict(id=2, job_id="CP-6014-11937", user_id=2, email="bob.c@test.com", phone="206-555-0178", confirmation_no="WMC-000002"), + dict(id=3, job_id="CP-2073-12751", user_id=3, email="carol.d@test.com", phone="253-555-0119", confirmation_no="WMC-000003"), + dict(id=4, job_id="R-2434655", user_id=4, email="david.k@test.com", phone="214-555-0166", confirmation_no="WMC-000004"), +] + + +class State: + """Mutable copy of the seeded users / saved_jobs / applications tables.""" + + def __init__(self) -> None: + self.users = copy.deepcopy(SEED_USERS) + self.saved = list(SEED_SAVED) + self.applications = copy.deepcopy(SEED_APPLICATIONS) + self.extra_sql: list[str] = [] + + # -- mutators ----------------------------------------------------------- + def add_user(self, email: str, **fields: Any) -> int: + new_id = max(u["id"] for u in self.users) + 1 + self.users.append(dict(id=new_id, email=email, username=email.split("@")[0], + first_name="New", last_name="Candidate", phone="", city="", state="", **fields)) + return new_id + + def set_profile(self, user_id: int, **fields: Any) -> None: + for user in self.users: + if user["id"] == user_id: + user.update(fields) + return + raise KeyError(user_id) + + def add_saved(self, user_id: int, job_id: str) -> None: + new_id = max(row[0] for row in self.saved) + 1 + self.saved.append((new_id, user_id, job_id)) + + def remove_saved(self, user_id: int, job_id: str) -> None: + before = len(self.saved) + self.saved = [row for row in self.saved if not (row[1] == user_id and row[2] == job_id)] + assert len(self.saved) == before - 1, f"no saved row {user_id}/{job_id}" + + def add_application(self, job_id: str, user_id: int | None, email: str, phone: str) -> str: + new_id = max(a["id"] for a in self.applications) + 1 + confirmation = f"WMC-{new_id:06d}" + self.applications.append(dict(id=new_id, job_id=job_id, user_id=user_id, email=email, + phone=phone, confirmation_no=confirmation)) + return confirmation + + # -- persistence -------------------------------------------------------- + def write(self, path: Path) -> Path: + shutil.copy2(SEED_DB, path) + connection = sqlite3.connect(path) + try: + connection.execute("PRAGMA foreign_keys=ON") + connection.execute("DELETE FROM saved_jobs") + connection.execute("DELETE FROM applications") + keep_ids = {int(user["id"]) for user in self.users} + for row in connection.execute("SELECT id FROM users").fetchall(): + if int(row[0]) not in keep_ids: + connection.execute("DELETE FROM users WHERE id = ?", (row[0],)) + for user in self.users: + existing = connection.execute("SELECT 1 FROM users WHERE id = ?", (user["id"],)).fetchone() + values = { + **user, + "display_name": f"{user['first_name']} {user['last_name']}".strip(), + "password_hash": "scrypt:32768:8:1$fixture$invalid", + "created_at": "2026-08-01 00:00:00", + } + if existing: + connection.execute( + "UPDATE users SET email=:email, username=:username, display_name=:display_name, " + "first_name=:first_name, last_name=:last_name, phone=:phone, city=:city, state=:state WHERE id=:id", + values, + ) + else: + connection.execute( + "INSERT INTO users(id,email,username,display_name,first_name,last_name,phone,city,state,password_hash,created_at) " + "VALUES (:id,:email,:username,:display_name,:first_name,:last_name,:phone,:city,:state,:password_hash,:created_at)", + values, + ) + connection.executemany( + "INSERT INTO saved_jobs(id, user_id, job_id, saved_at) VALUES (?, ?, ?, '2026-08-01 00:00:00')", + self.saved, + ) + for application in self.applications: + connection.execute( + "INSERT INTO applications(id, job_id, user_id, email, first_name, last_name, phone, status, confirmation_no, submitted_at) " + "VALUES (:id, :job_id, :user_id, :email, 'F', 'L', :phone, 'Submitted', :confirmation_no, '2026-08-01 00:00:00')", + application, + ) + for statement in self.extra_sql: + connection.execute(statement) + connection.commit() + finally: + connection.close() + return path + + +def step(path: str, action: str = "click", text: str | None = None) -> dict[str, Any]: + """One trajectory step in the agent.py shape; ``path`` is relative to BASE.""" + params: dict[str, Any] = {"text": text} if text is not None else {} + url = path if path.startswith("http") else f"{BASE}{path}" + return {"url": url, "action": action, "params": params} + + +def login_steps(email: str) -> list[dict[str, Any]]: + return [ + step("/login", "input", email), + step("/login", "input", PASSWORD), + step("/login", "click"), + ] + + +def only_paths(steps: list[dict[str, Any]], *allowed: str) -> list[dict[str, Any]]: + """Keep the steps whose URL path is one of ``allowed`` (shortcut trajectories).""" + from urllib.parse import urlparse + + def path_of(item: dict[str, Any]) -> str: + return urlparse(item["url"]).path.rstrip("/") or "/" + + return [item for item in steps if path_of(item) in allowed] + + +def write_run(run_dir: Path, task_id: str, steps: list[dict[str, Any]], answer: str) -> None: + run_dir.mkdir(parents=True, exist_ok=True) + shots = run_dir / "screenshots" + shots.mkdir(exist_ok=True) + png = base64.b64decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=") + numbered = [] + for index, item in enumerate(steps): + before = f"step_{index:03d}_before.png" + after = f"step_{index:03d}_after.png" + (shots / before).write_bytes(png) + (shots / after).write_bytes(png) + numbered.append({"step": index, **item, "screenshot_before": before, "screenshot_after": after}) + trajectory = { + "task": "synthetic", "task_id": task_id, "start_url": f"{BASE}/", "model": "unit-test", + "max_steps": 30, "steps": numbered, "terminated": bool(answer), + "termination_reason": "agent_done" if answer else "max_steps", + "final_url": numbered[-1]["url"] if numbered else f"{BASE}/", + "final_answer": answer if answer else None, + } + (run_dir / "trajectory.json").write_text(json.dumps(trajectory, indent=2), encoding="utf-8") + + +class VerifierTestCase(unittest.TestCase): + """Base class: ``self.N`` selects verify_N.py.""" + + N = -1 + + @property + def task_id(self) -> str: + return f"Walmart Careers--{self.N}" + + def verdict( + self, + steps: list[dict[str, Any]], + answer: str, + initial: State | None = None, + after: State | None = None, + task_id: str | None = None, + snapshots_in_run_dir: bool = False, + trajectory_updates: dict[str, Any] | None = None, + corrupt_screenshot: bool = False, + ) -> dict[str, Any]: + initial = initial or State() + after = after or State() + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + run_dir = root / "run" + write_run(run_dir, task_id or self.task_id, steps, answer) + if trajectory_updates: + trajectory_path = run_dir / "trajectory.json" + trajectory = json.loads(trajectory_path.read_text()) + trajectory.update(trajectory_updates) + trajectory_path.write_text(json.dumps(trajectory, indent=2)) + if corrupt_screenshot: + first = next((run_dir / "screenshots").glob("*.png")) + first.write_bytes(b"not a png") + if snapshots_in_run_dir: + initial.write(run_dir / "initial.db") + after.write(run_dir / "after.db") + command = [sys.executable, str(VERIFY_DIR / f"verify_{self.N}.py"), "--run_dir", str(run_dir)] + else: + command = [ + sys.executable, str(VERIFY_DIR / f"verify_{self.N}.py"), "--run_dir", str(run_dir), + "--initial_db", str(initial.write(root / "initial.db")), + "--after_db", str(after.write(root / "after.db")), + ] + result = subprocess.run(command, capture_output=True, text=True) + self.assertTrue(result.stdout.strip(), f"verifier printed nothing; stderr={result.stderr}") + verdict = json.loads(result.stdout) + verdict["returncode"] = result.returncode + return verdict + + def assertPasses(self, verdict: dict[str, Any]) -> None: + self.assertTrue(verdict["pass"], verdict["evidence"]) + self.assertEqual(verdict["returncode"], 0) + self.assertEqual(verdict["reason"], "all checks passed") + + def assertFailsOn(self, verdict: dict[str, Any], reason: str) -> None: + self.assertFalse(verdict["pass"], verdict["evidence"]) + self.assertEqual(verdict["returncode"], 1) + self.assertEqual(verdict["reason"], reason, verdict["evidence"]) diff --git a/sites/walmart_careers/verify/tests/test_verify_0.py b/sites/walmart_careers/verify/tests/test_verify_0.py new file mode 100644 index 00000000..86a4d115 --- /dev/null +++ b/sites/walmart_careers/verify/tests/test_verify_0.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import State, VerifierTestCase, login_steps, only_paths, step # noqa: E402,F401 + +GENUINE_STEPS = [ + step("/"), + step("/", "input", "optician"), + step("/results?q=optician"), + step("/jobs/CP-5991-12522", "done"), +] +ANSWER = 'CP-5991-12522 / 2441 S Rock Rd' + + +def genuine_after() -> State: + after = State() + pass + return after + + +class VerifyTask0Tests(VerifierTestCase): + N = 0 + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after())) + + def test_run_dir_snapshots_are_discovered(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), snapshots_in_run_dir=True) + self.assertPasses(verdict) + + def test_noop_run_fails_on_empty_answer(self) -> None: + self.assertFailsOn(self.verdict([step("/")], ""), "final_answer_nonempty") + + def test_other_task_trajectory_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), task_id="Walmart Careers--99") + self.assertFailsOn(verdict, "trajectory_task_matches") + + def test_shortcut_fails_on_gate(self) -> None: + steps = [step("/"), step("/", "input", "optician"), step("/results?q=optician", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), 'visited_job_detail_CP-5991-12522') + + def test_wrong_answer_0_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'CP-5133-12610 / 3030 N Rock Rd', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_requisition_id') + + def test_wrong_answer_1_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'CP-5991-12522 / 3030 N Rock Rd', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_street_address') + + def test_read_only_write_fails(self) -> None: + initial = State() + after = genuine_after() + after.add_saved(2, "CP-5991-12522") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'read_only_saved_jobs_unchanged') + + def test_street_synonyms_and_search_alias_pass(self) -> None: + steps = [step("/"), step("/results?searchQuery=Optician"), step("/jobs/CP-5991-12522", "done")] + self.assertPasses(self.verdict(steps, "Req ID cp-5991-12522, 2441 South Rock Road, Wichita")) + + def test_missing_results_visit_fails(self) -> None: + steps = [step("/"), step("/jobs/CP-5991-12522", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_results_optician_search") + + def test_unterminated_run_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, trajectory_updates={"terminated": False}) + self.assertFailsOn(verdict, "trajectory_completed") + + def test_mixed_origin_run_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, trajectory_updates={"start_url": "http://127.0.0.1:41023/"}) + self.assertFailsOn(verdict, "all_urls_match_local_origin") + + def test_corrupt_screenshot_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, corrupt_screenshot=True) + self.assertFailsOn(verdict, "screenshots_decode") + + def test_schema_change_fails_closed(self) -> None: + after = State() + after.extra_sql.append("CREATE TABLE injected(id INTEGER PRIMARY KEY)") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, after=after), "snapshot_contract_invalid") + + def test_catalog_change_fails_closed(self) -> None: + after = State() + after.extra_sql.append("UPDATE jobs SET title='tampered' WHERE job_id='CP-5991-12522'") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, after=after), "snapshot_contract_invalid") + + def test_wrong_seed_marker_fails_closed(self) -> None: + initial = State() + initial.extra_sql.append("UPDATE seed_metadata SET value='wrong' WHERE key='version'") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial), "snapshot_contract_invalid") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/walmart_careers/verify/tests/test_verify_1.py b/sites/walmart_careers/verify/tests/test_verify_1.py new file mode 100644 index 00000000..68303eb0 --- /dev/null +++ b/sites/walmart_careers/verify/tests/test_verify_1.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import State, VerifierTestCase, login_steps, only_paths, step # noqa: E402,F401 + +GENUINE_STEPS = [ + step("/"), + step("/", "input", "staff software engineer"), + step("/results?q=staff+software+engineer"), + step("/jobs/R-2468347", "done"), +] +ANSWER = "Option 2: 7 years' experience in software engineering or related area, including experience operating search or ML-serving systems in production." + + +def genuine_after() -> State: + after = State() + pass + return after + + +class VerifyTask1Tests(VerifierTestCase): + N = 1 + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after())) + + def test_run_dir_snapshots_are_discovered(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), snapshots_in_run_dir=True) + self.assertPasses(verdict) + + def test_noop_run_fails_on_empty_answer(self) -> None: + self.assertFailsOn(self.verdict([step("/")], ""), "final_answer_nonempty") + + def test_other_task_trajectory_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), task_id="Walmart Careers--99") + self.assertFailsOn(verdict, "trajectory_task_matches") + + def test_shortcut_fails_on_gate(self) -> None: + steps = [step("/"), step("/results?q=staff+software+engineer", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), 'visited_job_detail_R-2468347') + + def test_wrong_answer_0_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, "Option 1: Bachelor's degree in computer science, computer engineering, computer information systems, software engineering, or related area and 5 years' experience in software engineering or related area, including experience operating search or ML-serving systems in production.", after=genuine_after()) + self.assertFailsOn(verdict, 'answer_quotes_option_2') + + def test_wrong_answer_1_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, "7 years' experience in software engineering or related area, including experience building high-volume checkout or payments services.", after=genuine_after()) + self.assertFailsOn(verdict, 'answer_quotes_option_2') + + def test_read_only_application_fails(self) -> None: + initial = State() + after = genuine_after() + after.add_application("CP-9054-10921", 2, "bob.c@test.com", "206-555-0178") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'read_only_applications_unchanged') + + def test_curly_quotes_and_case_pass(self) -> None: + answer = "7 YEARS’ EXPERIENCE IN SOFTWARE ENGINEERING OR RELATED AREA, INCLUDING EXPERIENCE OPERATING SEARCH OR ML-SERVING SYSTEMS IN PRODUCTION." + self.assertPasses(self.verdict(GENUINE_STEPS, answer)) + + def test_truncated_option_two_quote_fails(self) -> None: + answer = "7 years' experience operating search or ML-serving systems in production" + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_quotes_option_2") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/walmart_careers/verify/tests/test_verify_10.py b/sites/walmart_careers/verify/tests/test_verify_10.py new file mode 100644 index 00000000..45eb34d1 --- /dev/null +++ b/sites/walmart_careers/verify/tests/test_verify_10.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import State, VerifierTestCase, login_steps, only_paths, step # noqa: E402,F401 + +GENUINE_STEPS = [ + step("/"), + step("/results?q=delivery+search+arrival+matching"), + step("/jobs/R-2456729"), + step("/results?q=delivery+search+arrival+matching"), + step("/jobs/R-2443374", "done"), +] +ANSWER = 'R-2456729 / 9 years' + + +def genuine_after() -> State: + after = State() + pass + return after + + +class VerifyTask10Tests(VerifierTestCase): + N = 10 + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after())) + + def test_run_dir_snapshots_are_discovered(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), snapshots_in_run_dir=True) + self.assertPasses(verdict) + + def test_noop_run_fails_on_empty_answer(self) -> None: + self.assertFailsOn(self.verdict([step("/")], ""), "final_answer_nonempty") + + def test_other_task_trajectory_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), task_id="Walmart Careers--99") + self.assertFailsOn(verdict, "trajectory_task_matches") + + def test_shortcut_fails_on_gate(self) -> None: + steps = [step("/"), step("/results?q=delivery+search"), step("/jobs/R-2456729", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), 'visited_job_detail_R-2443374') + + def test_wrong_answer_0_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'R-2443374 / 6 years', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_requisition_id') + + def test_wrong_answer_1_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'R-2456729 / 6 years', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_years_count') + + def test_read_only_write_fails(self) -> None: + initial = State() + after = genuine_after() + after.add_saved(2, "CP-5991-12522") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'read_only_saved_jobs_unchanged') + + def test_years_word_form_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, "R-2456729 (Bentonville) requires nine years under Option 2")) + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/walmart_careers/verify/tests/test_verify_11.py b/sites/walmart_careers/verify/tests/test_verify_11.py new file mode 100644 index 00000000..4904e3f6 --- /dev/null +++ b/sites/walmart_careers/verify/tests/test_verify_11.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import State, VerifierTestCase, login_steps, only_paths, step # noqa: E402,F401 + +GENUINE_STEPS = [ + step("/"), + *login_steps("alice.j@test.com"), + step("/", "input", "yard driver"), + step("/results?q=yard+driver"), + step("/jobs/CP-6088-10659"), + step("/jobs/CP-6088-10659", "navigate"), + step("/candidate-home/saved-roles", "done"), +] +ANSWER = 'Saved the Yard Driver-Off Property posting in Williamsburg, VA' + + +def genuine_after() -> State: + after = State() + after.add_saved(1, "CP-6088-10659") + return after + + +class VerifyTask11Tests(VerifierTestCase): + N = 11 + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after())) + + def test_run_dir_snapshots_are_discovered(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), snapshots_in_run_dir=True) + self.assertPasses(verdict) + + def test_noop_run_fails_on_empty_answer(self) -> None: + self.assertFailsOn(self.verdict([step("/")], ""), "final_answer_nonempty") + + def test_other_task_trajectory_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), task_id="Walmart Careers--99") + self.assertFailsOn(verdict, "trajectory_task_matches") + + def test_shortcut_fails_on_gate(self) -> None: + steps = only_paths(GENUINE_STEPS, "/", "/results", "/login") + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), 'visited_job_detail_CP-6088-10659') + + def test_state_unchanged_fails(self) -> None: + initial = State() + after = genuine_after() + after = State() + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'target_saved_for_alice') + + def test_extra_role_saved_fails(self) -> None: + initial = State() + after = genuine_after() + after.add_saved(1, "CP-6088-11595") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'alice_saved_roles_changed_only_by_target') + + def test_wrong_role_saved_fails(self) -> None: + initial = State() + after = genuine_after() + after = State(); after.add_saved(1, "CP-6088-11595") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'target_saved_for_alice') + + def test_saved_by_other_account_fails(self) -> None: + initial = State() + after = genuine_after() + after = State(); after.add_saved(2, "CP-6088-10659") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'target_saved_for_alice') + + def test_application_side_effect_fails(self) -> None: + initial = State() + after = genuine_after() + after.add_application("CP-6088-10659", 1, "alice.j@test.com", "479-555-0134") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'applications_unchanged') + + def test_already_saved_precondition_fails(self) -> None: + initial = State() + after = genuine_after() + initial.add_saved(1, "CP-6088-10659") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'initial_target_not_saved') + + def test_wrong_account_login_fails(self) -> None: + steps = [step("/"), *login_steps("bob.c@test.com"), step("/results?q=yard+driver"), step("/jobs/CP-6088-10659", "done")] + after = State() + after.add_saved(1, "CP-6088-10659") + self.assertFailsOn(self.verdict(steps, ANSWER, after=after), "entered_expected_account_email") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/walmart_careers/verify/tests/test_verify_12.py b/sites/walmart_careers/verify/tests/test_verify_12.py new file mode 100644 index 00000000..9cd97889 --- /dev/null +++ b/sites/walmart_careers/verify/tests/test_verify_12.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import State, VerifierTestCase, login_steps, only_paths, step # noqa: E402,F401 + +GENUINE_STEPS = [ + step("/"), + *login_steps("bob.c@test.com"), + step("/account"), + step("/candidate-home/saved-roles"), + step("/candidate-home/saved-roles", "done"), +] +ANSWER = 'Removed the Asset Protection Associate role at Neighborhood Market #5991' + + +def genuine_after() -> State: + after = State() + after.remove_saved(2, "CP-5991-11940") + return after + + +class VerifyTask12Tests(VerifierTestCase): + N = 12 + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after())) + + def test_run_dir_snapshots_are_discovered(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), snapshots_in_run_dir=True) + self.assertPasses(verdict) + + def test_noop_run_fails_on_empty_answer(self) -> None: + self.assertFailsOn(self.verdict([step("/")], ""), "final_answer_nonempty") + + def test_other_task_trajectory_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), task_id="Walmart Careers--99") + self.assertFailsOn(verdict, "trajectory_task_matches") + + def test_shortcut_fails_on_gate(self) -> None: + steps = [step("/"), *login_steps("bob.c@test.com"), step("/account", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), 'visited_saved_roles_page') + + def test_state_unchanged_fails(self) -> None: + initial = State() + after = genuine_after() + after = State() + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'target_removed_for_bob') + + def test_wrong_role_removed_fails(self) -> None: + initial = State() + after = genuine_after() + after = State(); after.remove_saved(2, "CP-6038-12357") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'target_removed_for_bob') + + def test_two_roles_removed_fails(self) -> None: + initial = State() + after = genuine_after() + after.remove_saved(2, "CP-6038-12357") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'bob_saved_roles_changed_only_by_target') + + def test_other_users_roles_touched_fails(self) -> None: + initial = State() + after = genuine_after() + after.remove_saved(1, "CP-5991-12522") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), "saved_jobs_exact_delta") + + def test_application_side_effect_fails(self) -> None: + initial = State() + after = genuine_after() + after.add_application("CP-5991-11940", 2, "bob.c@test.com", "206-555-0178") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'applications_unchanged') + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/walmart_careers/verify/tests/test_verify_13.py b/sites/walmart_careers/verify/tests/test_verify_13.py new file mode 100644 index 00000000..caca1ddf --- /dev/null +++ b/sites/walmart_careers/verify/tests/test_verify_13.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import State, VerifierTestCase, login_steps, only_paths, step # noqa: E402,F401 + +GENUINE_STEPS = [ + step("/"), + *login_steps("carol.d@test.com"), + step("/", "input", "pharmacy technician"), + step("/results?q=pharmacy+technician"), + step("/jobs/CP-4137-10959"), + step("/jobs/CP-4137-10959/apply", "input", "253-555-0142"), + step("/jobs/CP-4137-10959/apply"), + step("/jobs/CP-4137-10959/apply/confirm"), + step("/jobs/CP-4137-10959/apply/submitted", "done"), +] +ANSWER = 'Confirmation number WMC-000005' + + +def genuine_after() -> State: + after = State() + after.add_application("CP-4137-10959", 3, "carol.d@test.com", "(253) 555-0142") + return after + + +class VerifyTask13Tests(VerifierTestCase): + N = 13 + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after())) + + def test_run_dir_snapshots_are_discovered(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), snapshots_in_run_dir=True) + self.assertPasses(verdict) + + def test_noop_run_fails_on_empty_answer(self) -> None: + self.assertFailsOn(self.verdict([step("/")], ""), "final_answer_nonempty") + + def test_other_task_trajectory_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), task_id="Walmart Careers--99") + self.assertFailsOn(verdict, "trajectory_task_matches") + + def test_shortcut_fails_on_gate(self) -> None: + steps = only_paths(GENUINE_STEPS, "/", "/results", "/login") + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), 'visited_job_detail_CP-4137-10959') + + def test_wrong_answer_0_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'Confirmation number WMC-000006', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_matching_confirmation_number') + + def test_wrong_answer_1_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'Application submitted', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_matching_confirmation_number') + + def test_state_unchanged_fails(self) -> None: + initial = State() + after = genuine_after() + after = State() + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'exactly_one_new_application') + + def test_wrong_phone_fails(self) -> None: + initial = State() + after = genuine_after() + after = State(); after.add_application("CP-4137-10959", 3, "carol.d@test.com", "253-555-0119") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'new_application_has_task_phone') + + def test_wrong_posting_fails(self) -> None: + initial = State() + after = genuine_after() + after = State(); after.add_application("CP-5260-11531", 3, "carol.d@test.com", "253-555-0142") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'new_application_is_for_target_posting') + + def test_anonymous_application_fails(self) -> None: + initial = State() + after = genuine_after() + after = State(); after.add_application("CP-4137-10959", None, "carol.d@test.com", "253-555-0142") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'new_application_belongs_to_carol') + + def test_two_applications_fail(self) -> None: + initial = State() + after = genuine_after() + after.add_application("CP-4137-10959", 3, "carol.d@test.com", "253-555-0142") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'exactly_one_new_application') + + def test_existing_application_mutation_fails(self) -> None: + after = genuine_after() + after.applications[0]["phone"] = "999-555-1212" + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, after=after), "applications_exact_delta") + + def test_missing_confirm_page_fails(self) -> None: + steps = [item for item in GENUINE_STEPS if not item["url"].endswith("/apply/confirm")] + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), "visited_apply_confirm_page") + + def test_skipped_reset_still_matches_row(self) -> None: + initial = State() + initial.add_application("CP-2503-11505", 1, "alice.j@test.com", "479-555-0134") # WMC-000005 already used + after = State() + after.add_application("CP-2503-11505", 1, "alice.j@test.com", "479-555-0134") + confirmation = after.add_application("CP-4137-10959", 3, "carol.d@test.com", "253-555-0142") + self.assertEqual(confirmation, "WMC-000006") + self.assertPasses(self.verdict(GENUINE_STEPS, f"Your confirmation number is {confirmation}.", initial=initial, after=after)) + self.assertFailsOn(self.verdict(GENUINE_STEPS, "WMC-000005", initial=initial, after=after), "answer_has_matching_confirmation_number") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/walmart_careers/verify/tests/test_verify_14.py b/sites/walmart_careers/verify/tests/test_verify_14.py new file mode 100644 index 00000000..56926dd2 --- /dev/null +++ b/sites/walmart_careers/verify/tests/test_verify_14.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import State, VerifierTestCase, login_steps, only_paths, step # noqa: E402,F401 + +GENUINE_STEPS = [ + step("/"), + step("/register", "input", "new.candidate@test.com"), + step("/register", "input", "BenchPass123!"), + step("/register"), + step("/candidate-home/saved-roles", "input", "ecom warehouse worker"), + step("/results?q=ecom+warehouse+worker"), + step("/jobs/CP-9046-11274"), + step("/jobs/CP-9046-11274", "done"), +] +ANSWER = 'Registered new.candidate@test.com and saved CP-9046-11274' + + +def genuine_after() -> State: + after = State() + new_id = after.add_user("new.candidate@test.com"); after.add_saved(new_id, "CP-9046-11274") + return after + + +class VerifyTask14Tests(VerifierTestCase): + N = 14 + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after())) + + def test_run_dir_snapshots_are_discovered(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), snapshots_in_run_dir=True) + self.assertPasses(verdict) + + def test_noop_run_fails_on_empty_answer(self) -> None: + self.assertFailsOn(self.verdict([step("/")], ""), "final_answer_nonempty") + + def test_other_task_trajectory_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), task_id="Walmart Careers--99") + self.assertFailsOn(verdict, "trajectory_task_matches") + + def test_shortcut_fails_on_gate(self) -> None: + steps = only_paths(GENUINE_STEPS, "/", "/results", "/candidate-home/saved-roles", "/jobs/CP-9046-11274") + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), 'visited_register_page') + + def test_state_unchanged_fails(self) -> None: + initial = State() + after = genuine_after() + after = State() + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'new_user_registered') + + def test_seeded_account_used_fails(self) -> None: + initial = State() + after = genuine_after() + after = State(); after.add_saved(1, "CP-9046-11274") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'new_user_registered') + + def test_new_user_saved_wrong_posting_fails(self) -> None: + initial = State() + after = genuine_after() + after = State(); uid = after.add_user("new.candidate@test.com"); after.add_saved(uid, "CP-9046-10913") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'target_saved_by_new_user') + + def test_new_user_but_seeded_account_saved_fails(self) -> None: + initial = State() + after = genuine_after() + after = State(); after.add_user("new.candidate@test.com"); after.add_saved(1, "CP-9046-11274") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'target_saved_by_new_user') + + def test_seeded_user_also_gained_target_fails(self) -> None: + initial = State() + after = genuine_after() + after.add_saved(2, "CP-9046-11274") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'no_seeded_user_gained_target') + + def test_application_side_effect_fails(self) -> None: + initial = State() + after = genuine_after() + after.add_application("CP-9046-11274", None, "new.candidate@test.com", "555-555-5555") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'applications_unchanged') + + def test_detail_page_gate_required(self) -> None: + steps = [step("/"), step("/register", "input", "new.candidate@test.com"), step("/register"), step("/results?q=ecom", "done")] + after = State() + uid = after.add_user("new.candidate@test.com") + after.add_saved(uid, "CP-9046-11274") + self.assertFailsOn(self.verdict(steps, ANSWER, after=after), "visited_job_detail_CP-9046-11274") + + def test_multiple_new_users_fail(self) -> None: + after = genuine_after() + after.add_user("second@test.com") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, after=after), "new_user_registered") + + def test_new_user_extra_save_fails(self) -> None: + after = genuine_after() + after.add_saved(5, "CP-9046-10913") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, after=after), "target_saved_by_new_user") + + def test_existing_user_mutation_fails(self) -> None: + after = genuine_after() + after.set_profile(1, city="Changed") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, after=after), "users_exact_delta") + + def test_registration_after_job_visit_fails_order(self) -> None: + steps = [step("/"), step("/jobs/CP-9046-11274"), *GENUINE_STEPS[1:4]] + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), "workflow_in_order") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/walmart_careers/verify/tests/test_verify_15.py b/sites/walmart_careers/verify/tests/test_verify_15.py new file mode 100644 index 00000000..8529a705 --- /dev/null +++ b/sites/walmart_careers/verify/tests/test_verify_15.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import State, VerifierTestCase, login_steps, only_paths, step # noqa: E402,F401 + +GENUINE_STEPS = [ + step("/"), + *login_steps("david.k@test.com"), + step("/account"), + step("/account/edit", "input", "Rogers"), + step("/account/edit", "input", "AR"), + step("/account/edit"), + step("/account"), + step("/account"), + step("/candidate-home/applications", "done"), +] +ANSWER = 'Profile updated to Rogers, AR / existing application WMC-000004' + + +def genuine_after() -> State: + after = State() + after.set_profile(4, city="Rogers", state="AR") + return after + + +class VerifyTask15Tests(VerifierTestCase): + N = 15 + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after())) + + def test_run_dir_snapshots_are_discovered(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), snapshots_in_run_dir=True) + self.assertPasses(verdict) + + def test_noop_run_fails_on_empty_answer(self) -> None: + self.assertFailsOn(self.verdict([step("/")], ""), "final_answer_nonempty") + + def test_other_task_trajectory_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), task_id="Walmart Careers--99") + self.assertFailsOn(verdict, "trajectory_task_matches") + + def test_shortcut_fails_on_gate(self) -> None: + steps = [step("/"), *login_steps("david.k@test.com"), step("/account/edit", "input", "Rogers"), step("/account/edit", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), 'visited_applications_page') + + def test_wrong_answer_0_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'Profile updated / WMC-000001', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_existing_confirmation_number') + + def test_wrong_answer_1_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'Profile updated / WMC-000005', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_existing_confirmation_number') + + def test_state_unchanged_fails(self) -> None: + initial = State() + after = genuine_after() + after = State() + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'profile_city_and_state_updated') + + def test_only_city_changed_fails(self) -> None: + initial = State() + after = genuine_after() + after = State(); after.set_profile(4, city="Rogers") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'profile_city_and_state_updated') + + def test_new_application_side_effect_fails(self) -> None: + initial = State() + after = genuine_after() + after.add_application("R-2434655", 4, "david.k@test.com", "214-555-0166") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'applications_unchanged') + + def test_other_user_edited_fails(self) -> None: + initial = State() + after = genuine_after() + after.set_profile(1, city="Rogers", state="AR") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'other_users_unchanged') + + def test_lowercase_state_and_city_accepted(self) -> None: + after = State() + after.set_profile(4, city="rogers", state="AR") + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER, after=after)) + + def test_preexisting_requested_profile_noop_fails(self) -> None: + initial = State(); initial.set_profile(4, city="Rogers", state="AR") + after = State(); after.set_profile(4, city="Rogers", state="AR") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), "initial_profile_requires_update") + + def test_other_profile_field_change_fails(self) -> None: + after = State() + after.set_profile(4, city="Rogers", state="AR", phone="999-555-1212") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, after=after), "profile_exact_delta") + + def test_applications_before_edit_fails_order(self) -> None: + steps = [step("/"), *login_steps("david.k@test.com"), step("/candidate-home/applications"), + step("/account/edit", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), "workflow_in_order") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/walmart_careers/verify/tests/test_verify_16.py b/sites/walmart_careers/verify/tests/test_verify_16.py new file mode 100644 index 00000000..af3819af --- /dev/null +++ b/sites/walmart_careers/verify/tests/test_verify_16.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import State, VerifierTestCase, login_steps, only_paths, step # noqa: E402,F401 + +GENUINE_STEPS = [ + step("/"), + step("/results"), + step("/results?loc=Puerto+Rico&radius=25"), + step("/results?q=cashier&shift=Weekday+Day&rate=Hourly&loc=Puerto+Rico&radius=25"), + step("/jobs/CP-2503-11505", "navigate"), + step("/jobs/CP-2610-11040", "navigate"), + step("/jobs/CP-2503-10981", "navigate"), + step("/jobs/CP-2503-10981", "done"), +] +ANSWER = 'CP-2503-10981 / 5 open positions' + + +def genuine_after() -> State: + after = State() + pass + return after + + +class VerifyTask16Tests(VerifierTestCase): + N = 16 + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after())) + + def test_run_dir_snapshots_are_discovered(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), snapshots_in_run_dir=True) + self.assertPasses(verdict) + + def test_noop_run_fails_on_empty_answer(self) -> None: + self.assertFailsOn(self.verdict([step("/")], ""), "final_answer_nonempty") + + def test_other_task_trajectory_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), task_id="Walmart Careers--99") + self.assertFailsOn(verdict, "trajectory_task_matches") + + def test_shortcut_fails_on_gate(self) -> None: + steps = [step("/"), step("/results?shift=Weekday+Day&rate=Hourly"), step("/jobs/CP-2503-10981", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), 'visited_results_required_filters') + + def test_wrong_answer_0_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'CP-2503-11505 / 1 open position', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_requisition_id') + + def test_wrong_answer_1_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'CP-2503-10981 / 1 open position', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_positions_count') + + def test_read_only_write_fails(self) -> None: + initial = State() + after = genuine_after() + after.add_saved(2, "CP-5991-12522") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'read_only_saved_jobs_unchanged') + + def test_city_location_does_not_substitute_for_puerto_rico_scope(self) -> None: + steps = [step("/"), step("/results?q=cashier&shift=Weekday+Day&rate=Hourly&loc=Bayamon%2C+PR&radius=60"), step("/jobs/CP-2503-10981", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_results_required_filters") + + def test_shift_filter_missing_fails(self) -> None: + steps = [step("/"), step("/results?rate=Hourly&loc=Puerto+Rico&radius=25"), step("/jobs/CP-2503-10981", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_results_required_filters") + + def test_detail_visit_of_other_cashier_only_fails(self) -> None: + steps = [step("/"), step("/results?q=cashier&shift=Weekday+Day&rate=Hourly&loc=Puerto+Rico&radius=25"), step("/jobs/CP-2503-11505", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_job_detail_CP-2503-10981") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/walmart_careers/verify/tests/test_verify_17.py b/sites/walmart_careers/verify/tests/test_verify_17.py new file mode 100644 index 00000000..47d61a28 --- /dev/null +++ b/sites/walmart_careers/verify/tests/test_verify_17.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import State, VerifierTestCase, login_steps, only_paths, step # noqa: E402,F401 + +GENUINE_STEPS = [ + step("/"), + *login_steps("alice.j@test.com"), + step("/candidate-home/saved-roles"), + step("/jobs/CP-9046-10913", "navigate"), + step("/jobs/CP-2503-11505"), + step("/jobs/CP-2503-11505/apply"), + step("/jobs/CP-2503-11505/apply/confirm"), + step("/jobs/CP-2503-11505/apply/submitted", "done"), +] +ANSWER = 'Shift may start between 6:00am - 11:00am / confirmation WMC-000005' + + +def genuine_after() -> State: + after = State() + after.add_application("CP-2503-11505", 1, "alice.j@test.com", "479-555-0134") + return after + + +class VerifyTask17Tests(VerifierTestCase): + N = 17 + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after())) + + def test_run_dir_snapshots_are_discovered(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), snapshots_in_run_dir=True) + self.assertPasses(verdict) + + def test_noop_run_fails_on_empty_answer(self) -> None: + self.assertFailsOn(self.verdict([step("/")], ""), "final_answer_nonempty") + + def test_other_task_trajectory_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), task_id="Walmart Careers--99") + self.assertFailsOn(verdict, "trajectory_task_matches") + + def test_shortcut_fails_on_gate(self) -> None: + steps = only_paths(GENUINE_STEPS, "/", "/results", "/login") + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), 'visited_saved_roles_page') + + def test_wrong_answer_0_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'Shift may start between 12:00pm - 5:00pm / confirmation WMC-000005', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_shift_window') + + def test_wrong_answer_1_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'Shift may start between 6:00am - 11:00am / confirmation WMC-000009', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_matching_confirmation_number') + + def test_state_unchanged_fails(self) -> None: + initial = State() + after = genuine_after() + after = State() + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'exactly_one_new_application') + + def test_applied_to_full_time_role_fails(self) -> None: + initial = State() + after = genuine_after() + after = State(); after.add_application("CP-9046-10913", 1, "alice.j@test.com", "479-555-0134") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'new_application_is_for_part_time_saved_role') + + def test_applied_with_other_email_fails(self) -> None: + initial = State() + after = genuine_after() + after = State(); after.add_application("CP-2503-11505", 1, "someone@else.com", "479-555-0134") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'new_application_belongs_to_alice') + + def test_preexisting_target_application_fails(self) -> None: + initial = State(); initial.add_application("CP-2503-11505", 1, "alice.j@test.com", "479-555-0134") + after = State(); after.add_application("CP-2503-11505", 1, "alice.j@test.com", "479-555-0134") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), "initial_has_no_alice_application_for_target") + + def test_saved_role_mutation_fails(self) -> None: + after = genuine_after() + after.remove_saved(1, "CP-9046-10913") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, after=after), "saved_jobs_unchanged") + + def test_missing_confirm_page_fails(self) -> None: + steps = [item for item in GENUINE_STEPS if not item["url"].endswith("/apply/confirm")] + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), "visited_apply_confirm_page") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/walmart_careers/verify/tests/test_verify_18.py b/sites/walmart_careers/verify/tests/test_verify_18.py new file mode 100644 index 00000000..5b3e27c1 --- /dev/null +++ b/sites/walmart_careers/verify/tests/test_verify_18.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import State, VerifierTestCase, login_steps, only_paths, step # noqa: E402,F401 + +GENUINE_STEPS = [ + step("/"), + step("/careers-areas/supply-chain-and-transportation"), + step("/results?area=supply-chain-and-transportation&category=drivers"), + step("/jobs/CP-6014-13829"), + step("/results?area=supply-chain-and-transportation&category=drivers"), + step("/jobs/CP-6088-11595", "done"), +] +ANSWER = 'CP-6014-13829 / Shift may start between 5:00am - 10:00am / 4 open positions' + + +def genuine_after() -> State: + after = State() + pass + return after + + +class VerifyTask18Tests(VerifierTestCase): + N = 18 + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after())) + + def test_run_dir_snapshots_are_discovered(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), snapshots_in_run_dir=True) + self.assertPasses(verdict) + + def test_noop_run_fails_on_empty_answer(self) -> None: + self.assertFailsOn(self.verdict([step("/")], ""), "final_answer_nonempty") + + def test_other_task_trajectory_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), task_id="Walmart Careers--99") + self.assertFailsOn(verdict, "trajectory_task_matches") + + def test_shortcut_fails_on_gate(self) -> None: + steps = [step("/"), step("/results?area=supply-chain-and-transportation&category=drivers"), step("/jobs/CP-6014-13829"), step("/jobs/CP-6088-11595", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), 'visited_supply_chain_area_page') + + def test_wrong_answer_0_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'CP-6088-11595 / Shift may start between 8:00pm - 1:00am / 3 open positions', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_requisition_id') + + def test_wrong_answer_1_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'CP-6014-13829 / Shift may start between 8:00pm - 1:00am / 4 open positions', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_shift_window') + + def test_wrong_answer_2_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'CP-6014-13829 / Shift may start between 5:00am - 10:00am / 3 open positions', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_positions_count') + + def test_read_only_write_fails(self) -> None: + initial = State() + after = genuine_after() + after.add_saved(2, "CP-5991-12522") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'read_only_saved_jobs_unchanged') + + def test_one_detail_page_only_fails(self) -> None: + steps = [step("/"), step("/careers-areas/supply-chain-and-transportation"), step("/results?area=supply-chain-and-transportation&category=drivers"), step("/jobs/CP-6014-13829", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_job_detail_CP-6088-11595") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/walmart_careers/verify/tests/test_verify_19.py b/sites/walmart_careers/verify/tests/test_verify_19.py new file mode 100644 index 00000000..db2284eb --- /dev/null +++ b/sites/walmart_careers/verify/tests/test_verify_19.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import State, VerifierTestCase, login_steps, only_paths, step # noqa: E402,F401 + +GENUINE_STEPS = [ + step("/"), + *login_steps("bob.c@test.com"), + step("/careers-areas/stores-and-clubs"), + step("/results?area=stores-and-clubs&category=digital-pickup-and-delivery"), + step("/results?area=stores-and-clubs&category=digital-pickup-and-delivery&type=Full+time"), + step("/jobs/CP-144-10765", "navigate"), + step("/jobs/CP-2073-11104", "navigate"), + step("/jobs/CP-2075-11715", "navigate"), + step("/jobs/CP-3593-12490", "navigate"), + step("/jobs/CP-3826-11166", "navigate"), + step("/jobs/CP-5260-10596", "navigate"), + step("/jobs/CP-1179-11202"), + step("/jobs/CP-1179-11202", "done"), +] +ANSWER = 'CP-1179-11202 / 1301 SW Wanamaker Rd' + + +def genuine_after() -> State: + after = State() + after.add_saved(2, "CP-1179-11202") + return after + + +class VerifyTask19Tests(VerifierTestCase): + N = 19 + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after())) + + def test_run_dir_snapshots_are_discovered(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), snapshots_in_run_dir=True) + self.assertPasses(verdict) + + def test_noop_run_fails_on_empty_answer(self) -> None: + self.assertFailsOn(self.verdict([step("/")], ""), "final_answer_nonempty") + + def test_other_task_trajectory_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), task_id="Walmart Careers--99") + self.assertFailsOn(verdict, "trajectory_task_matches") + + def test_shortcut_fails_on_gate(self) -> None: + steps = only_paths(GENUINE_STEPS, "/", "/results", "/login", "/careers-areas/stores-and-clubs") + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), 'visited_job_detail_CP-1179-11202') + + def test_wrong_answer_0_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'CP-2073-11104 / 10000 Brookpark Rd', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_requisition_id') + + def test_wrong_answer_1_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'CP-1179-11202 / 10000 Brookpark Rd', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_street_address') + + def test_state_unchanged_fails(self) -> None: + initial = State() + after = genuine_after() + after = State() + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'target_saved_for_bob') + + def test_extra_role_saved_fails(self) -> None: + initial = State() + after = genuine_after() + after.add_saved(2, "CP-2073-11104") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'bob_saved_roles_changed_only_by_target') + + def test_application_side_effect_fails(self) -> None: + initial = State() + after = genuine_after() + after.add_application("CP-1179-11202", 2, "bob.c@test.com", "206-555-0178") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'applications_unchanged') + + def test_full_time_filter_missing_fails(self) -> None: + steps = [step("/"), *login_steps("bob.c@test.com"), step("/careers-areas/stores-and-clubs"), + step("/results?area=stores-and-clubs&category=digital-pickup-and-delivery"), step("/jobs/CP-1179-11202", "done")] + after = State() + after.add_saved(2, "CP-1179-11202") + self.assertFailsOn(self.verdict(steps, ANSWER, after=after), "visited_digital_pickup_full_time_results") + + def test_preexisting_target_noop_fails(self) -> None: + initial = State(); initial.add_saved(2, "CP-1179-11202") + after = State(); after.add_saved(2, "CP-1179-11202") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), "initial_target_not_saved") + + def test_other_users_saved_roles_change_fails(self) -> None: + after = genuine_after() + after.add_saved(1, "CP-6088-10659") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, after=after), "saved_jobs_exact_delta") + + def test_area_after_results_fails_order(self) -> None: + steps = [step("/"), *login_steps("bob.c@test.com"), + step("/results?category=digital-pickup-and-delivery&type=Full+time"), + step("/careers-areas/stores-and-clubs"), + *[step(f"/jobs/{job_id}") for job_id in ("CP-1179-11202", "CP-144-10765", "CP-2073-11104", "CP-2075-11715", "CP-3593-12490", "CP-3826-11166", "CP-5260-10596")]] + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), "workflow_in_order") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/walmart_careers/verify/tests/test_verify_2.py b/sites/walmart_careers/verify/tests/test_verify_2.py new file mode 100644 index 00000000..5d3da54d --- /dev/null +++ b/sites/walmart_careers/verify/tests/test_verify_2.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import State, VerifierTestCase, login_steps, only_paths, step # noqa: E402,F401 + +GENUINE_STEPS = [ + step("/"), + step("/", "input", "freight handler"), + step("/results?q=freight+handler"), + step("/jobs/CP-9054-10921", "done"), +] +ANSWER = 'Shift may start between 6:00pm - 3:00am / 3 open positions' + + +def genuine_after() -> State: + after = State() + pass + return after + + +class VerifyTask2Tests(VerifierTestCase): + N = 2 + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after())) + + def test_run_dir_snapshots_are_discovered(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), snapshots_in_run_dir=True) + self.assertPasses(verdict) + + def test_noop_run_fails_on_empty_answer(self) -> None: + self.assertFailsOn(self.verdict([step("/")], ""), "final_answer_nonempty") + + def test_other_task_trajectory_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), task_id="Walmart Careers--99") + self.assertFailsOn(verdict, "trajectory_task_matches") + + def test_shortcut_fails_on_gate(self) -> None: + steps = [step("/"), step("/results?q=freight+handler", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), 'visited_job_detail_CP-9054-10921') + + def test_wrong_answer_0_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'Shift may start between 9:00pm - 1:30am / 4 open positions', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_shift_window') + + def test_wrong_answer_1_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'Shift may start between 6:00pm - 3:00am / 4 open positions', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_positions_count') + + def test_read_only_write_fails(self) -> None: + initial = State() + after = genuine_after() + after.add_saved(2, "CP-5991-12522") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'read_only_saved_jobs_unchanged') + + def test_count_attached_to_years_fails(self) -> None: + answer = "The shift is 6:00pm - 3:00am and the role requires 3 years of experience." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_positions_count") + + def test_time_format_variants_pass(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, "Starts 6 PM to 3 a.m.; three positions")) + self.assertPasses(self.verdict(GENUINE_STEPS, "18:00-03:00, 3 openings")) + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/walmart_careers/verify/tests/test_verify_3.py b/sites/walmart_careers/verify/tests/test_verify_3.py new file mode 100644 index 00000000..b5d0dae2 --- /dev/null +++ b/sites/walmart_careers/verify/tests/test_verify_3.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import State, VerifierTestCase, login_steps, only_paths, step # noqa: E402,F401 + +GENUINE_STEPS = [ + step("/"), + step("/careers-areas/healthcare"), + step("/results?area=healthcare&category=pharmacy-services"), + step("/jobs/CP-5260-11531", "done"), +] +ANSWER = '#pharmacytechjobs / 2 open positions' + + +def genuine_after() -> State: + after = State() + pass + return after + + +class VerifyTask3Tests(VerifierTestCase): + N = 3 + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after())) + + def test_run_dir_snapshots_are_discovered(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), snapshots_in_run_dir=True) + self.assertPasses(verdict) + + def test_noop_run_fails_on_empty_answer(self) -> None: + self.assertFailsOn(self.verdict([step("/")], ""), "final_answer_nonempty") + + def test_other_task_trajectory_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), task_id="Walmart Careers--99") + self.assertFailsOn(verdict, "trajectory_task_matches") + + def test_shortcut_fails_on_gate(self) -> None: + steps = [step("/"), step("/results?area=healthcare&category=pharmacy-services"), step("/jobs/CP-5260-11531", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), 'visited_healthcare_area_page') + + def test_wrong_answer_0_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, '#pharmacytechjobs / 3 open positions', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_positions_count') + + def test_wrong_answer_1_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, '#pharmacyjobs / 2 open positions', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_hashtag') + + def test_read_only_write_fails(self) -> None: + initial = State() + after = genuine_after() + after.add_saved(2, "CP-5991-12522") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'read_only_saved_jobs_unchanged') + + def test_area_only_results_gate_passes(self) -> None: + steps = [step("/"), step("/careers-areas/healthcare"), step("/results?area=healthcare"), step("/jobs/CP-5260-11531", "done")] + self.assertPasses(self.verdict(steps, ANSWER)) + + def test_results_without_area_or_category_fails(self) -> None: + steps = [step("/"), step("/careers-areas/healthcare"), step("/results?q=pharmacy"), step("/jobs/CP-5260-11531", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_healthcare_results") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/walmart_careers/verify/tests/test_verify_4.py b/sites/walmart_careers/verify/tests/test_verify_4.py new file mode 100644 index 00000000..2bc7a651 --- /dev/null +++ b/sites/walmart_careers/verify/tests/test_verify_4.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import State, VerifierTestCase, login_steps, only_paths, step # noqa: E402,F401 + +GENUINE_STEPS = [ + step("/"), + step("/results"), + step("/results?brand=Sam%27s+Club&type=Part+time&shift=Weekend+Overnight"), + step("/jobs/CP-4750-11130", "done"), +] +ANSWER = 'CP-4750-11130 / 3 open positions' + + +def genuine_after() -> State: + after = State() + pass + return after + + +class VerifyTask4Tests(VerifierTestCase): + N = 4 + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after())) + + def test_run_dir_snapshots_are_discovered(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), snapshots_in_run_dir=True) + self.assertPasses(verdict) + + def test_noop_run_fails_on_empty_answer(self) -> None: + self.assertFailsOn(self.verdict([step("/")], ""), "final_answer_nonempty") + + def test_other_task_trajectory_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), task_id="Walmart Careers--99") + self.assertFailsOn(verdict, "trajectory_task_matches") + + def test_shortcut_fails_on_gate(self) -> None: + steps = [step("/"), step("/results"), step("/jobs/CP-4750-11130", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), 'visited_results_required_filters') + + def test_wrong_answer_0_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'CP-4750-11229 / 2 open positions', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_requisition_id') + + def test_wrong_answer_1_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'CP-4750-11130 / 2 open positions', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_positions_count') + + def test_read_only_write_fails(self) -> None: + initial = State() + after = genuine_after() + after.add_saved(2, "CP-5991-12522") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'read_only_saved_jobs_unchanged') + + def test_shift_filter_alone_fails_gate(self) -> None: + steps = [step("/"), step("/results?shift=Weekend+Overnight"), step("/jobs/CP-4750-11130", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_results_required_filters") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/walmart_careers/verify/tests/test_verify_5.py b/sites/walmart_careers/verify/tests/test_verify_5.py new file mode 100644 index 00000000..6268cb70 --- /dev/null +++ b/sites/walmart_careers/verify/tests/test_verify_5.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import State, VerifierTestCase, login_steps, only_paths, step # noqa: E402,F401 + +GENUINE_STEPS = [ + step("/"), + step("/results"), + step("/results?area=technology&type=Full+time"), + step("/results?area=technology&type=Full+time&loc=Hoboken%2C+NJ&radius=25"), + step("/jobs/R-2411489", "done"), +] +ANSWER = "R-2411489 / Option 1: Bachelor's degree in computer science, computer information systems, or related area" + + +def genuine_after() -> State: + after = State() + pass + return after + + +class VerifyTask5Tests(VerifierTestCase): + N = 5 + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after())) + + def test_run_dir_snapshots_are_discovered(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), snapshots_in_run_dir=True) + self.assertPasses(verdict) + + def test_noop_run_fails_on_empty_answer(self) -> None: + self.assertFailsOn(self.verdict([step("/")], ""), "final_answer_nonempty") + + def test_other_task_trajectory_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), task_id="Walmart Careers--99") + self.assertFailsOn(verdict, "trajectory_task_matches") + + def test_shortcut_fails_on_gate(self) -> None: + steps = [step("/"), step("/results?area=technology&type=Full+time&loc=Hoboken%2C+NJ&radius=25", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), 'visited_job_detail_R-2411489') + + def test_wrong_answer_0_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, "R-2411489 / Master's degree in computer science", after=genuine_after()) + self.assertFailsOn(verdict, 'answer_names_option_1_degree') + + def test_wrong_answer_1_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, "R-2424873 / Bachelor's degree in computer science", after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_requisition_id') + + def test_read_only_write_fails(self) -> None: + initial = State() + after = genuine_after() + after.add_saved(2, "CP-5991-12522") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'read_only_saved_jobs_unchanged') + + def test_location_only_results_gate_fails(self) -> None: + steps = [step("/"), step("/results?q=engineer&loc=hoboken&radius=25"), step("/jobs/R-2411489", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_results_required_filters") + + def test_plain_search_results_fail_gate(self) -> None: + steps = [step("/"), step("/results?q=senior+software+engineer"), step("/jobs/R-2411489", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_results_required_filters") + + def test_typed_search_without_filters_fails_gate(self) -> None: + steps = [step("/"), step("/results?q=Technology+Hoboken%2C+NJ"), step("/jobs/R-2411489", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_results_required_filters") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/walmart_careers/verify/tests/test_verify_6.py b/sites/walmart_careers/verify/tests/test_verify_6.py new file mode 100644 index 00000000..9fe2f2e5 --- /dev/null +++ b/sites/walmart_careers/verify/tests/test_verify_6.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import State, VerifierTestCase, login_steps, only_paths, step # noqa: E402,F401 + +GENUINE_STEPS = [ + step("/"), + step("/results"), + step("/results?loc=Cleveland%2C+OH&radius=25"), + step("/results?type=Full+time&shift=Weekday+Day&loc=Cleveland%2C+OH&radius=25"), + step("/jobs/CP-2073-11104", "done"), +] +ANSWER = '10000 Brookpark Rd / 2 open positions' + + +def genuine_after() -> State: + after = State() + pass + return after + + +class VerifyTask6Tests(VerifierTestCase): + N = 6 + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after())) + + def test_run_dir_snapshots_are_discovered(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), snapshots_in_run_dir=True) + self.assertPasses(verdict) + + def test_noop_run_fails_on_empty_answer(self) -> None: + self.assertFailsOn(self.verdict([step("/")], ""), "final_answer_nonempty") + + def test_other_task_trajectory_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), task_id="Walmart Careers--99") + self.assertFailsOn(verdict, "trajectory_task_matches") + + def test_shortcut_fails_on_gate(self) -> None: + steps = [step("/"), step("/results?type=Full+time&shift=Weekday+Day"), step("/jobs/CP-2073-11104", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), 'visited_results_required_filters') + + def test_wrong_answer_0_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, '3400 Steelyard Dr / 2 open positions', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_street_address') + + def test_wrong_answer_1_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, '10000 Brookpark Rd / 3 open positions', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_positions_count') + + def test_read_only_write_fails(self) -> None: + initial = State() + after = genuine_after() + after.add_saved(2, "CP-5991-12522") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'read_only_saved_jobs_unchanged') + + def test_street_number_not_counted_as_positions(self) -> None: + self.assertFailsOn(self.verdict(GENUINE_STEPS, "10000 Brookpark Road"), "answer_has_positions_count") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/walmart_careers/verify/tests/test_verify_7.py b/sites/walmart_careers/verify/tests/test_verify_7.py new file mode 100644 index 00000000..56c6921d --- /dev/null +++ b/sites/walmart_careers/verify/tests/test_verify_7.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import State, VerifierTestCase, login_steps, only_paths, step # noqa: E402,F401 + +GENUINE_STEPS = [ + step("/"), + step("/results"), + step("/results?area=students&brand=Sam%27s+Club&type=Intern"), + step("/jobs/R-2447168", "done"), +] +ANSWER = 'Intern (Fixed Term) / 2101 SE Simple Savings Dr' + + +def genuine_after() -> State: + after = State() + pass + return after + + +class VerifyTask7Tests(VerifierTestCase): + N = 7 + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after())) + + def test_run_dir_snapshots_are_discovered(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), snapshots_in_run_dir=True) + self.assertPasses(verdict) + + def test_noop_run_fails_on_empty_answer(self) -> None: + self.assertFailsOn(self.verdict([step("/")], ""), "final_answer_nonempty") + + def test_other_task_trajectory_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), task_id="Walmart Careers--99") + self.assertFailsOn(verdict, "trajectory_task_matches") + + def test_shortcut_fails_on_gate(self) -> None: + steps = [step("/"), step("/results?area=students&brand=Sam%27s+Club&type=Intern", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), 'visited_job_detail_R-2447168') + + def test_wrong_answer_0_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'Regular/Permanent / 702 SW 8th St', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_worker_type_chip') + + def test_wrong_answer_1_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'Intern (Fixed Term) / 702 SW 8th St', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_street_address') + + def test_read_only_write_fails(self) -> None: + initial = State() + after = genuine_after() + after.add_saved(2, "CP-5991-12522") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'read_only_saved_jobs_unchanged') + + def test_required_filter_missing_fails_gate(self) -> None: + for query in ("area=students&type=Intern", "area=students&brand=Sam%27s+Club", "type=Intern&brand=Sam%27s+Club"): + with self.subTest(query=query): + steps = [step("/"), step("/results?" + query), step("/jobs/R-2447168", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_results_required_filters") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/walmart_careers/verify/tests/test_verify_8.py b/sites/walmart_careers/verify/tests/test_verify_8.py new file mode 100644 index 00000000..43d675cc --- /dev/null +++ b/sites/walmart_careers/verify/tests/test_verify_8.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import State, VerifierTestCase, login_steps, only_paths, step # noqa: E402,F401 + +GENUINE_STEPS = [ + step("/"), + step("/", "input", "auto care center technician"), + step("/results?q=auto+care+center+technician"), + step("/jobs/CP-1230-11592"), + step("/results?q=auto+care+center+technician"), + step("/jobs/CP-954-10637", "done"), +] +ANSWER = 'Store #1230 with 5 open positions' + + +def genuine_after() -> State: + after = State() + pass + return after + + +class VerifyTask8Tests(VerifierTestCase): + N = 8 + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after())) + + def test_run_dir_snapshots_are_discovered(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), snapshots_in_run_dir=True) + self.assertPasses(verdict) + + def test_noop_run_fails_on_empty_answer(self) -> None: + self.assertFailsOn(self.verdict([step("/")], ""), "final_answer_nonempty") + + def test_other_task_trajectory_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), task_id="Walmart Careers--99") + self.assertFailsOn(verdict, "trajectory_task_matches") + + def test_shortcut_fails_on_gate(self) -> None: + steps = [step("/"), step("/results?q=auto+care+center+technician"), step("/jobs/CP-1230-11592", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), 'visited_job_detail_CP-954-10637') + + def test_wrong_answer_0_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'Store #954 with 3 open positions', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_names_winning_store_number') + + def test_wrong_answer_1_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'Store #1230 with 3 open positions', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_positions_count') + + def test_equivalent_comparison_by_loser_passes(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'Store 1230 has 5 open positions; store 954 has 3 open positions, so #954 has fewer.', after=genuine_after()) + self.assertPasses(verdict) + + def test_read_only_write_fails(self) -> None: + initial = State() + after = genuine_after() + after.add_saved(2, "CP-5991-12522") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'read_only_saved_jobs_unchanged') + + def test_store_number_inside_requisition_id_does_not_count(self) -> None: + self.assertFailsOn(self.verdict(GENUINE_STEPS, "CP-1230-11592 has 5 openings"), "answer_names_winning_store_number") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/walmart_careers/verify/tests/test_verify_9.py b/sites/walmart_careers/verify/tests/test_verify_9.py new file mode 100644 index 00000000..72c69b84 --- /dev/null +++ b/sites/walmart_careers/verify/tests/test_verify_9.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import State, VerifierTestCase, login_steps, only_paths, step # noqa: E402,F401 + +GENUINE_STEPS = [ + step("/"), + step("/results?q=freight+handler"), + step("/jobs/CP-6038-10642"), + step("/results?q=freight+handler"), + step("/jobs/CP-9046-10913", "done"), +] +ANSWER = 'CP-6038-10642 / Shift may start between 3:00pm - 7:30pm' + + +def genuine_after() -> State: + after = State() + pass + return after + + +class VerifyTask9Tests(VerifierTestCase): + N = 9 + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after())) + + def test_run_dir_snapshots_are_discovered(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), snapshots_in_run_dir=True) + self.assertPasses(verdict) + + def test_noop_run_fails_on_empty_answer(self) -> None: + self.assertFailsOn(self.verdict([step("/")], ""), "final_answer_nonempty") + + def test_other_task_trajectory_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=genuine_after(), task_id="Walmart Careers--99") + self.assertFailsOn(verdict, "trajectory_task_matches") + + def test_shortcut_fails_on_gate(self) -> None: + steps = [step("/"), step("/results?q=freight+handler"), step("/jobs/CP-6038-10642", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER, after=genuine_after()), 'visited_job_detail_CP-9046-10913') + + def test_wrong_answer_0_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'CP-9046-10913 / Shift may start between 9:00pm - 1:30am', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_requisition_id') + + def test_wrong_answer_1_fails(self) -> None: + verdict = self.verdict(GENUINE_STEPS, 'CP-6038-10642 / Shift may start between 9:00pm - 1:30am', after=genuine_after()) + self.assertFailsOn(verdict, 'answer_has_shift_window') + + def test_read_only_write_fails(self) -> None: + initial = State() + after = genuine_after() + after.add_saved(2, "CP-5991-12522") + self.assertFailsOn(self.verdict(GENUINE_STEPS, ANSWER, initial=initial, after=after), 'read_only_saved_jobs_unchanged') + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/walmart_careers/verify/tests/test_verify_lib.py b/sites/walmart_careers/verify/tests/test_verify_lib.py new file mode 100644 index 00000000..7005e3ba --- /dev/null +++ b/sites/walmart_careers/verify/tests/test_verify_lib.py @@ -0,0 +1,206 @@ +from __future__ import annotations + +import re +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from _support import State # noqa: E402 +from verify_lib import ( # noqa: E402 + application_rows, + contains_confirmation_number, + contains_count, + contains_hashtag, + contains_req_id, + contains_shift_window, + contains_street, + is_walmart_careers_site_url, + job_detail_visited, + mentions_store_number, + navigated_to_path, + new_application_rows, + new_user_ids, + results_visited, + rows_unchanged_except, + saved_job_ids, + saved_jobs_delta, + tables_unchanged, + trajectory_last_email, + user_profile, +) + + +def traj(*urls: str) -> dict: + return {"steps": [{"url": url, "action": "click", "params": {}} for url in urls]} + + +class UrlGateTests(unittest.TestCase): + def test_loopback_origins_on_any_port(self) -> None: + for url in ( + "http://localhost:40023/jobs/CP-1-1", + "http://127.0.0.1:41023/jobs/CP-1-1?x=1", + "http://[::1]:5017/jobs/CP-1-1", + ): + with self.subTest(url=url): + self.assertTrue(is_walmart_careers_site_url(url)) + self.assertTrue(navigated_to_path(traj(url), "/jobs/CP-1-1")) + + def test_external_and_non_http_rejected(self) -> None: + for url in ("https://careers.walmart.com/jobs/CP-1-1", "/jobs/CP-1-1", "file:///jobs/CP-1-1"): + with self.subTest(url=url): + self.assertFalse(is_walmart_careers_site_url(url)) + self.assertFalse(navigated_to_path(traj(url), "/jobs/CP-1-1")) + + def test_job_detail_is_exact(self) -> None: + self.assertTrue(job_detail_visited(traj("http://localhost:41023/jobs/CP-5991-12522/"), "CP-5991-12522")) + self.assertFalse(job_detail_visited(traj("http://localhost:41023/jobs/CP-5991-12522/apply"), "CP-5991-12522")) + self.assertFalse(job_detail_visited(traj("http://localhost:41023/jobs/CP-5991-11940"), "CP-5991-12522")) + + def test_start_url_counts(self) -> None: + self.assertTrue(navigated_to_path({"start_url": "http://localhost:40023/", "steps": []}, "/")) + + def test_results_text_params(self) -> None: + t = traj("http://localhost:41023/results?q=Yard+Driver+roles") + self.assertTrue(results_visited(t, q="yard")) + self.assertTrue(results_visited(traj("http://localhost:41023/results?searchQuery=yard"), q="yard")) + self.assertFalse(results_visited(t, q="optician")) + self.assertFalse(results_visited(traj("http://localhost:41023/?q=yard"), q="yard")) + + def test_results_facets_are_exact(self) -> None: + t = traj("http://localhost:41023/results?brand=Sam%27s+Club&type=Part+time&shift=Weekend+Overnight&loc=Plano%2C+TX&radius=25") + self.assertTrue(results_visited(t, shift="Weekend Overnight", type="Part time", brand="Sam's Club")) + self.assertTrue(results_visited(t, loc="plano")) + self.assertFalse(results_visited(t, shift="Weekend")) + self.assertFalse(results_visited(t, type="Full time")) + + def test_results_alternatives_and_regex(self) -> None: + alternatives = ("puerto rico", re.compile(r"\bpr\b")) + self.assertTrue(results_visited(traj("http://localhost:41023/results?loc=Bayamon%2C+PR"), loc=alternatives)) + self.assertTrue(results_visited(traj("http://localhost:41023/results?loc=puerto+rico"), loc=alternatives)) + self.assertFalse(results_visited(traj("http://localhost:41023/results?loc=Springfield"), loc=alternatives)) + + def test_last_email_input(self) -> None: + t = {"steps": [ + {"action": "input", "params": {"text": "alice.j@test.com"}}, + {"action": "input", "params": {"text": "TestPass123!"}}, + {"action": "input", "params": {"text": "Bob.C@test.com"}}, + ]} + self.assertEqual(trajectory_last_email(t), "bob.c@test.com") + + +class MatcherTests(unittest.TestCase): + def test_requisition_ids(self) -> None: + self.assertTrue(contains_req_id("id cp–5991–12522.", "CP-5991-12522")) + self.assertTrue(contains_req_id("CP - 954 - 10637", "CP-954-10637")) + self.assertFalse(contains_req_id("CP-5991-125220", "CP-5991-12522")) + self.assertFalse(contains_req_id("XCP-5991-12522", "CP-5991-12522")) + self.assertFalse(contains_req_id("R-2468347", "R-2411489")) + self.assertFalse(contains_req_id("The requisition is not CP-5991-12522.", "CP-5991-12522")) + + def test_streets(self) -> None: + self.assertTrue(contains_street("2441 South Rock Road", "2441 S Rock Rd")) + self.assertTrue(contains_street("2101 Southeast Simple Savings Drive", "2101 SE Simple Savings Dr")) + self.assertTrue(contains_street("8109 Merrimac Trl", "8109 Merrimac Trail")) + self.assertTrue(contains_street("Carr 2 KM 11.4, Bayamon", "Carr 2 KM 11.4")) + self.assertFalse(contains_street("2441 S Maize Rd", "2441 S Rock Rd")) + self.assertFalse(contains_street("12441 S Rock Rd", "2441 S Rock Rd")) + self.assertFalse(contains_street("The address is not 2441 S Rock Rd.", "2441 S Rock Rd")) + self.assertFalse(contains_street("2441 Rock", "2441 S Rock Rd")) + self.assertFalse(contains_street("2441 S Rock", "2441 S Rock Rd")) + + def test_shift_windows(self) -> None: + self.assertTrue(contains_shift_window("6:00pm - 3:00am", "6:00pm", "3:00am")) + self.assertTrue(contains_shift_window("6 PM to 3 a.m.", "6:00pm", "3:00am")) + self.assertTrue(contains_shift_window("18:00–03:00", "6:00pm", "3:00am")) + self.assertTrue(contains_shift_window("noon to 5:00 pm", "12:00pm", "5:00pm")) + self.assertFalse(contains_shift_window("6:00am - 3:00am", "6:00pm", "3:00am")) + self.assertFalse(contains_shift_window("9:00pm - 1:30am", "3:00pm", "7:30pm")) + self.assertFalse(contains_shift_window("The window is not 6:00pm - 3:00am", "6:00pm", "3:00am")) + + def test_counts_ignore_ids_times_money_streets(self) -> None: + text = "CP-5991-12522 at 2441 S Rock Rd, store #5991, $17.00-$19.50/hr, 6:00pm-3:00am, zip 67207: 3 open positions" + self.assertTrue(contains_count(text, 3)) + for wrong in (5991, 12522, 2441, 17, 19, 6, 67207): + with self.subTest(wrong=wrong): + self.assertFalse(contains_count(text, wrong)) + self.assertTrue(contains_count("three open positions", 3)) + self.assertFalse(contains_count("Option 2 requires 9 years", 2)) + self.assertTrue(contains_count("Option 2 requires 9 years", 9)) + self.assertFalse(contains_count("3rd shift", 3)) + + def test_counts_survive_sentence_punctuation(self) -> None: + # Real nano runs on tasks 3 and 4 wrote the count right before a period. + self.assertTrue(contains_count("Hashtag: #pharmacytechjobs. Open positions: 2.", 2)) + self.assertTrue(contains_count("Requisition ID: CP-4750-11130; Open positions: 3.", 3)) + self.assertTrue(contains_count("Open positions: 2, hashtag #x", 2)) + self.assertFalse(contains_count("2.5 open positions", 2)) + self.assertFalse(contains_count("about 2,000 roles", 2)) + self.assertFalse(contains_count("12.", 2)) + self.assertFalse(contains_count("There are not 2 open positions.", 2)) + + def test_store_numbers(self) -> None: + self.assertTrue(mentions_store_number("Store #1230 has more", 1230)) + self.assertTrue(mentions_store_number("store 1230", 1230)) + self.assertFalse(mentions_store_number("CP-1230-11592 has 5", 1230)) + self.assertFalse(mentions_store_number("zip 39601", 3960)) + + def test_hashtags_and_confirmations(self) -> None: + self.assertTrue(contains_hashtag("ends with #PharmacyTechJobs.", "#pharmacytechjobs")) + self.assertFalse(contains_hashtag("#pharmacytechjobs2", "#pharmacytechjobs")) + self.assertTrue(contains_confirmation_number("Confirmation: wmc–000005", "WMC-000005")) + self.assertFalse(contains_confirmation_number("WMC-0000050", "WMC-000005")) + self.assertFalse(contains_confirmation_number("It is not WMC-000005.", "WMC-000005")) + + +class StateHelperTests(unittest.TestCase): + def setUp(self) -> None: + self.directory = tempfile.TemporaryDirectory() + root = Path(self.directory.name) + self.initial = State().write(root / "initial.db") + after_state = State() + after_state.add_saved(1, "CP-6088-10659") + after_state.remove_saved(2, "CP-5991-11940") + after_state.set_profile(4, city="Rogers", state="AR") + self.new_user = after_state.add_user("new.candidate@test.com") + self.confirmation = after_state.add_application("CP-4137-10959", 3, "carol.d@test.com", "(253) 555-0142") + self.after = after_state.write(root / "after.db") + + def tearDown(self) -> None: + self.directory.cleanup() + + def test_saved_jobs(self) -> None: + self.assertEqual(saved_jobs_delta(self.initial, self.after, "alice.j@test.com"), ({"CP-6088-10659"}, set())) + self.assertEqual(saved_jobs_delta(self.initial, self.after, "bob.c@test.com"), (set(), {"CP-5991-11940"})) + self.assertIsNone(saved_job_ids(self.initial, "nobody@test.com")) + self.assertEqual(len(saved_job_ids(self.initial, "alice.j@test.com")), 6) + + def test_applications(self) -> None: + rows = new_application_rows(self.initial, self.after) + self.assertEqual(len(rows), 1) + self.assertEqual(rows[0]["confirmation_no"], self.confirmation) + self.assertEqual(self.confirmation, "WMC-000005") + self.assertEqual(len(application_rows(self.after, job_id="CP-4137-10959", phone_digits="2535550142")), 1) + self.assertEqual(application_rows(self.after, job_id="CP-4137-10959", phone_digits="2535550119"), []) + self.assertEqual(application_rows(self.initial, email="DAVID.K@test.com")[0]["confirmation_no"], "WMC-000004") + + def test_users(self) -> None: + self.assertEqual(new_user_ids(self.initial, self.after), {self.new_user}) + self.assertEqual(user_profile(self.after, "david.k@test.com")["city"], "Rogers") + self.assertEqual(user_profile(self.initial, "david.k@test.com")["state"], "TX") + self.assertFalse(rows_unchanged_except(self.initial, self.after, "users", [])) + self.assertTrue(rows_unchanged_except(self.initial, self.after, "users", [4, self.new_user])) + + def test_tables_unchanged(self) -> None: + self.assertEqual( + tables_unchanged(self.initial, self.after, ("users", "saved_jobs", "applications")), + {"users": False, "saved_jobs": False, "applications": False}, + ) + self.assertEqual(tables_unchanged(self.initial, self.initial, ("saved_jobs",)), {"saved_jobs": True}) + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/walmart_careers/verify/verify_0.py b/sites/walmart_careers/verify/verify_0.py new file mode 100644 index 00000000..2243d052 --- /dev/null +++ b/sites/walmart_careers/verify/verify_0.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Verify Walmart Careers--0: Optician search; Wichita Neighborhood Market posting: requisition ID + street (read-only). + +Deterministic only: no LLM calls. Ground truth is hardcoded below and never +appears in tasks.jsonl. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + Judge, + check_read_only, + check_results_visited, + check_trajectory_identity, + check_visited_job_detail, + contains_req_id, + contains_street, + fail_closed, + final_answer, + load_run, + parse_args, + resolve_snapshots, +) + + +TASK_ID = "Walmart Careers--0" +JOB_ID = "CP-5991-12522" +STREET = "2441 S Rock Rd" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + from ground_truth import constants_for_task + globals().update(constants_for_task(initial_db, int(TASK_ID.rsplit("--", 1)[1]))) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + check_results_visited(judge, trajectory, "visited_results_optician_search", {"q": "optician"}) + check_visited_job_detail(judge, trajectory, JOB_ID) + judge.check( + "answer_has_requisition_id", + contains_req_id(answer, JOB_ID), + f"expected={JOB_ID!r}, answer={answer!r}", + ) + judge.check( + "answer_has_street_address", + contains_street(answer, STREET), + f"expected={STREET!r}, answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 — any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/walmart_careers/verify/verify_1.py b/sites/walmart_careers/verify/verify_1.py new file mode 100644 index 00000000..46a3c757 --- /dev/null +++ b/sites/walmart_careers/verify/verify_1.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Verify Walmart Careers--1: Staff, Software Engineer (Sunnyvale): quote Option 2 of Minimum Qualifications (read-only). + +Deterministic only: no LLM calls. Ground truth is hardcoded below and never +appears in tasks.jsonl. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + Judge, + check_read_only, + check_trajectory_identity, + check_visited_job_detail, + contains_all, + fail_closed, + final_answer, + load_run, + parse_args, + resolve_snapshots, +) + + +TASK_ID = "Walmart Careers--1" +JOB_ID = "R-2468347" +OPTION_2_EXACT = "7 years' experience in software engineering or related area, including experience operating search or ML-serving systems in production." +OPTION_1_EXACT = "Option 1: Bachelor's degree in computer science, computer engineering, computer information systems, software engineering, or related area and 5 years' experience in software engineering or related area, including experience operating search or ML-serving systems in production." + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + from ground_truth import constants_for_task + globals().update(constants_for_task(initial_db, int(TASK_ID.rsplit("--", 1)[1]))) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + check_visited_job_detail(judge, trajectory, JOB_ID) + judge.check( + "answer_quotes_option_2", + contains_all(answer, [OPTION_2_EXACT]), + f"expected_exact_option={OPTION_2_EXACT!r}, answer={answer!r}", + ) + quoted_option_1_only = contains_all(answer, [OPTION_1_EXACT]) and not contains_all(answer, [OPTION_2_EXACT]) + judge.check( + "answer_is_not_option_1", + not quoted_option_1_only, + f"option_1={OPTION_1_EXACT!r}, answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 — any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/walmart_careers/verify/verify_10.py b/sites/walmart_careers/verify/verify_10.py new file mode 100644 index 00000000..85c478ba --- /dev/null +++ b/sites/walmart_careers/verify/verify_10.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""Verify Walmart Careers--10: Senior Manager, Delivery Search postings (Bentonville vs Hoboken): more Option 2 years, requisition ID + years (read-only). + +Deterministic only: no LLM calls. Ground truth is hardcoded below and never +appears in tasks.jsonl. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + Judge, + check_read_only, + check_trajectory_identity, + check_visited_job_detail, + contains_years_count, + contains_req_id, + fail_closed, + final_answer, + load_run, + parse_args, + resolve_snapshots, +) + + +TASK_ID = "Walmart Careers--10" +JOB_ID = "R-2456729" +LOSER_ID = "R-2443374" +YEARS = 9 + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + from ground_truth import constants_for_task + globals().update(constants_for_task(initial_db, int(TASK_ID.rsplit("--", 1)[1]))) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + check_visited_job_detail(judge, trajectory, JOB_ID) + check_visited_job_detail(judge, trajectory, LOSER_ID) + judge.check( + "answer_has_requisition_id", + contains_req_id(answer, JOB_ID), + f"expected={JOB_ID!r}, answer={answer!r}", + ) + judge.check( + "answer_has_years_count", + contains_years_count(answer, YEARS), + f"expected={YEARS!r}, answer={answer!r}", + ) + judge.check( + "answer_does_not_report_other_posting_instead", + not (contains_req_id(answer, LOSER_ID) and not contains_req_id(answer, JOB_ID)), + f"winner={JOB_ID!r}, loser={LOSER_ID!r}, answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 — any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/walmart_careers/verify/verify_11.py b/sites/walmart_careers/verify/verify_11.py new file mode 100644 index 00000000..30475474 --- /dev/null +++ b/sites/walmart_careers/verify/verify_11.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""Verify Walmart Careers--11: Alice signs in, searches Yard Driver, saves the Williamsburg posting (stateful). + +Deterministic only: no LLM calls. Ground truth is hardcoded below and never +appears in tasks.jsonl. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + Judge, + check_results_visited, + check_signed_in_as, + check_tables_unchanged, + check_trajectory_identity, + check_visited_job_detail, + fail_closed, + load_run, + parse_args, + resolve_snapshots, + saved_job_ids, +) + + +TASK_ID = "Walmart Careers--11" +EMAIL = "alice.j@test.com" +JOB_ID = "CP-6088-10659" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + from ground_truth import constants_for_task + globals().update(constants_for_task(initial_db, int(TASK_ID.rsplit("--", 1)[1]))) + check_trajectory_identity(judge, trajectory, TASK_ID) + check_signed_in_as(judge, trajectory, EMAIL) + check_results_visited(judge, trajectory, "visited_results_yard_driver_search", {"q": "yard driver"}) + check_visited_job_detail(judge, trajectory, JOB_ID) + from verify_lib import check_paths_in_order + check_paths_in_order( + judge, trajectory, "workflow_in_order", + [("/login", {}), ("/results", {"q": "yard driver"}), (f"/jobs/{JOB_ID}", {})], + ) + before = saved_job_ids(initial_db, EMAIL) + after = saved_job_ids(after_db, EMAIL) + judge.check( + "initial_target_not_saved", + before is not None and JOB_ID not in before, + f"email={EMAIL}, job_id={JOB_ID}, initial_saved={sorted(before or set())!r}", + ) + judge.check( + "target_saved_for_alice", + after is not None and JOB_ID in after, + f"email={EMAIL}, job_id={JOB_ID}, after_saved={sorted(after or set())!r}", + ) + judge.check( + "alice_saved_roles_changed_only_by_target", + before is not None and after == before | {JOB_ID}, + f"initial_saved={sorted(before or set())!r}, after_saved={sorted(after or set())!r}", + ) + from verify_lib import table_delta, user_id_for_email + delta = table_delta(initial_db, after_db, "saved_jobs") + alice_id = user_id_for_email(initial_db, EMAIL) + judge.check( + "saved_jobs_exact_delta", + len(delta["added"]) == 1 and not delta["removed"] and not delta["changed"] + and delta["added"][0][1:3] == (alice_id, JOB_ID), + f"delta={delta!r}", + ) + check_tables_unchanged(judge, initial_db, after_db, ("users", "applications")) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 — any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/walmart_careers/verify/verify_12.py b/sites/walmart_careers/verify/verify_12.py new file mode 100644 index 00000000..e472fc51 --- /dev/null +++ b/sites/walmart_careers/verify/verify_12.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Verify Walmart Careers--12: Bob signs in, opens Saved roles, removes the Neighborhood Market role (stateful). + +Deterministic only: no LLM calls. Ground truth is hardcoded below and never +appears in tasks.jsonl. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + Judge, + check_signed_in_as, + check_tables_unchanged, + check_trajectory_identity, + check_visited_path, + fail_closed, + load_run, + parse_args, + resolve_snapshots, + saved_job_ids, +) + + +TASK_ID = "Walmart Careers--12" +EMAIL = "bob.c@test.com" +JOB_ID = "CP-5991-11940" +SAVED_ROLES_PATH = "/candidate-home/saved-roles" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + from ground_truth import constants_for_task + globals().update(constants_for_task(initial_db, int(TASK_ID.rsplit("--", 1)[1]))) + check_trajectory_identity(judge, trajectory, TASK_ID) + check_signed_in_as(judge, trajectory, EMAIL) + check_visited_path(judge, trajectory, "visited_saved_roles_page", SAVED_ROLES_PATH) + from verify_lib import check_paths_in_order + check_paths_in_order( + judge, trajectory, "workflow_in_order", [("/login", {}), (SAVED_ROLES_PATH, {})] + ) + before = saved_job_ids(initial_db, EMAIL) + after = saved_job_ids(after_db, EMAIL) + judge.check( + "initial_target_saved", + before is not None and JOB_ID in before, + f"email={EMAIL}, job_id={JOB_ID}, initial_saved={sorted(before or set())!r}", + ) + judge.check( + "target_removed_for_bob", + after is not None and JOB_ID not in after, + f"email={EMAIL}, job_id={JOB_ID}, after_saved={sorted(after or set())!r}", + ) + judge.check( + "bob_saved_roles_changed_only_by_target", + before is not None and after == before - {JOB_ID}, + f"initial_saved={sorted(before or set())!r}, after_saved={sorted(after or set())!r}", + ) + from verify_lib import table_delta, user_id_for_email + delta = table_delta(initial_db, after_db, "saved_jobs") + bob_id = user_id_for_email(initial_db, EMAIL) + judge.check( + "saved_jobs_exact_delta", + len(delta["removed"]) == 1 and not delta["added"] and not delta["changed"] + and delta["removed"][0][1:3] == (bob_id, JOB_ID), + f"delta={delta!r}", + ) + check_tables_unchanged(judge, initial_db, after_db, ("users", "applications")) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 — any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/walmart_careers/verify/verify_13.py b/sites/walmart_careers/verify/verify_13.py new file mode 100644 index 00000000..ec003dcf --- /dev/null +++ b/sites/walmart_careers/verify/verify_13.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +"""Verify Walmart Careers--13: Carol signs in and applies to the Tacoma Pharmacy Technician posting with the task phone; reports the confirmation number (stateful). + +Deterministic only: no LLM calls. Ground truth is hardcoded below and never +appears in tasks.jsonl. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + Judge, + application_rows, + check_signed_in_as, + check_trajectory_identity, + check_visited_job_detail, + check_visited_path, + contains_confirmation_number, + digits_only, + fail_closed, + final_answer, + load_run, + new_application_rows, + parse_args, + resolve_snapshots, +) + + +TASK_ID = "Walmart Careers--13" +EMAIL = "carol.d@test.com" +USER_ID = 3 +JOB_ID = "CP-4137-10959" +PHONE_DIGITS = "2535550142" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + from ground_truth import constants_for_task + globals().update(constants_for_task(initial_db, int(TASK_ID.rsplit("--", 1)[1]))) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + check_signed_in_as(judge, trajectory, EMAIL) + check_visited_job_detail(judge, trajectory, JOB_ID) + check_visited_path(judge, trajectory, "visited_apply_page", f"/jobs/{JOB_ID}/apply") + check_visited_path(judge, trajectory, "visited_apply_confirm_page", f"/jobs/{JOB_ID}/apply/confirm") + check_visited_path(judge, trajectory, "visited_apply_submitted_page", f"/jobs/{JOB_ID}/apply/submitted") + from verify_lib import check_paths_in_order + check_paths_in_order( + judge, trajectory, "workflow_in_order", + [("/login", {}), (f"/jobs/{JOB_ID}", {}), (f"/jobs/{JOB_ID}/apply", {}), + (f"/jobs/{JOB_ID}/apply/confirm", {}), (f"/jobs/{JOB_ID}/apply/submitted", {})], + ) + judge.check( + "initial_has_no_carol_application_for_target", + not application_rows(initial_db, job_id=JOB_ID, email=EMAIL), + f"job_id={JOB_ID}, email={EMAIL}", + ) + new_rows = new_application_rows(initial_db, after_db) + judge.check( + "exactly_one_new_application", + len(new_rows) == 1, + f"new_rows={new_rows!r}", + ) + row = new_rows[0] if len(new_rows) == 1 else {} + judge.check( + "new_application_is_for_target_posting", + row.get("job_id") == JOB_ID, + f"expected_job_id={JOB_ID!r}, row={row!r}", + ) + judge.check( + "new_application_belongs_to_carol", + row.get("user_id") == USER_ID and str(row.get("email") or "").casefold() == EMAIL, + f"expected_user_id={USER_ID}, expected_email={EMAIL!r}, row={row!r}", + ) + judge.check( + "new_application_has_task_phone", + digits_only(row.get("phone")) == PHONE_DIGITS, + f"expected_phone_digits={PHONE_DIGITS!r}, row_phone={row.get('phone')!r}", + ) + from verify_lib import trajectory_input_texts + judge.check( + "task_phone_entered_on_apply_flow", + any(digits_only(value) == PHONE_DIGITS for value in trajectory_input_texts(trajectory)), + f"expected_phone_digits={PHONE_DIGITS!r}", + ) + confirmation = str(row.get("confirmation_no") or "") + judge.check( + "answer_has_matching_confirmation_number", + bool(confirmation) and contains_confirmation_number(answer, confirmation), + f"row_confirmation_no={confirmation!r}, answer={answer!r}", + ) + from verify_lib import check_tables_unchanged, table_delta + delta = table_delta(initial_db, after_db, "applications") + judge.check( + "applications_exact_delta", + len(delta["added"]) == 1 and not delta["removed"] and not delta["changed"], + f"delta={delta!r}", + ) + check_tables_unchanged(judge, initial_db, after_db, ("users", "saved_jobs")) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 — any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/walmart_careers/verify/verify_14.py b/sites/walmart_careers/verify/verify_14.py new file mode 100644 index 00000000..000ebb45 --- /dev/null +++ b/sites/walmart_careers/verify/verify_14.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""Verify Walmart Careers--14: Register a new account, then save the eCom Warehouse Worker posting at #9046 Marcy (stateful). + +Deterministic only: no LLM calls. Ground truth is hardcoded below and never +appears in tasks.jsonl. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + Judge, + check_tables_unchanged, + check_trajectory_identity, + check_visited_job_detail, + check_visited_path, + fail_closed, + load_run, + new_user_ids, + parse_args, + resolve_snapshots, + saved_job_ids_by_user_id, + user_ids, +) + + +TASK_ID = "Walmart Careers--14" +JOB_ID = "CP-9046-11274" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + from ground_truth import constants_for_task + globals().update(constants_for_task(initial_db, int(TASK_ID.rsplit("--", 1)[1]))) + check_trajectory_identity(judge, trajectory, TASK_ID) + check_visited_path(judge, trajectory, "visited_register_page", "/register") + check_visited_job_detail(judge, trajectory, JOB_ID) + from verify_lib import check_paths_in_order + check_paths_in_order( + judge, trajectory, "workflow_in_order", [("/register", {}), (f"/jobs/{JOB_ID}", {})] + ) + fresh = new_user_ids(initial_db, after_db) + judge.check("new_user_registered", len(fresh) == 1, f"new_user_ids={sorted(fresh)!r}") + from verify_lib import new_user_emails, trajectory_last_email, trajectory_input_texts + fresh_emails = new_user_emails(initial_db, after_db) + judge.check( + "registered_email_matches_trajectory", + len(fresh_emails) == 1 and trajectory_last_email(trajectory) in fresh_emails, + f"new_emails={sorted(fresh_emails)!r}, entered_email={trajectory_last_email(trajectory)!r}", + ) + non_email_inputs = [value for value in trajectory_input_texts(trajectory) if "@" not in value] + judge.check( + "registration_password_entered", + any(8 <= len(value) <= 256 for value in non_email_inputs), + f"non_email_input_count={len(non_email_inputs)}", + ) + savers = sorted(uid for uid in fresh if JOB_ID in saved_job_ids_by_user_id(after_db, uid)) + judge.check( + "target_saved_by_new_user", + len(savers) == 1 and saved_job_ids_by_user_id(after_db, savers[0]) == {JOB_ID}, + f"job_id={JOB_ID}, new_user_ids={sorted(fresh)!r}, new_users_with_target={savers!r}", + ) + seeded_gainers = sorted( + uid + for uid in user_ids(initial_db) + if JOB_ID in saved_job_ids_by_user_id(after_db, uid) + and JOB_ID not in saved_job_ids_by_user_id(initial_db, uid) + ) + judge.check( + "no_seeded_user_gained_target", + not seeded_gainers, + f"seeded_users_that_gained_target={seeded_gainers!r}", + ) + from verify_lib import table_delta + user_delta = table_delta(initial_db, after_db, "users") + saved_delta = table_delta(initial_db, after_db, "saved_jobs") + new_id = next(iter(fresh)) if len(fresh) == 1 else None + judge.check( + "users_exact_delta", + len(user_delta["added"]) == 1 and not user_delta["removed"] and not user_delta["changed"] + and user_delta["added"][0][0] == new_id, + f"delta={user_delta!r}", + ) + judge.check( + "saved_jobs_exact_delta", + len(saved_delta["added"]) == 1 and not saved_delta["removed"] and not saved_delta["changed"] + and saved_delta["added"][0][1:3] == (new_id, JOB_ID), + f"delta={saved_delta!r}", + ) + check_tables_unchanged(judge, initial_db, after_db, ("applications",)) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 — any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/walmart_careers/verify/verify_15.py b/sites/walmart_careers/verify/verify_15.py new file mode 100644 index 00000000..aec5e3c6 --- /dev/null +++ b/sites/walmart_careers/verify/verify_15.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Verify Walmart Careers--15: David signs in, updates city/state, opens My applications, reports his existing confirmation number (stateful). + +Deterministic only: no LLM calls. Ground truth is hardcoded below and never +appears in tasks.jsonl. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + Judge, + check_signed_in_as, + check_tables_unchanged, + check_trajectory_identity, + check_visited_path, + contains_confirmation_number, + fail_closed, + final_answer, + load_run, + parse_args, + resolve_snapshots, + rows_unchanged_except, + user_id_for_email, + user_profile, +) + + +TASK_ID = "Walmart Careers--15" +EMAIL = "david.k@test.com" +CITY = "Rogers" +STATE = "AR" +CONFIRMATION_NO = "WMC-000004" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + from ground_truth import constants_for_task + globals().update(constants_for_task(initial_db, int(TASK_ID.rsplit("--", 1)[1]))) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + check_signed_in_as(judge, trajectory, EMAIL) + check_visited_path(judge, trajectory, "visited_account_edit_page", "/account/edit") + check_visited_path(judge, trajectory, "visited_applications_page", "/candidate-home/applications") + from verify_lib import check_paths_in_order + check_paths_in_order( + judge, trajectory, "workflow_in_order", + [("/login", {}), ("/account/edit", {}), ("/candidate-home/applications", {})], + ) + judge.check( + "answer_has_existing_confirmation_number", + contains_confirmation_number(answer, CONFIRMATION_NO), + f"expected={CONFIRMATION_NO!r}, answer={answer!r}", + ) + initial_profile = user_profile(initial_db, EMAIL) or {} + judge.check( + "initial_profile_requires_update", + str(initial_profile.get("city") or "").casefold() != CITY.casefold() + or str(initial_profile.get("state") or "").upper() != STATE, + f"initial_city={initial_profile.get('city')!r}, initial_state={initial_profile.get('state')!r}", + ) + profile = user_profile(after_db, EMAIL) or {} + judge.check( + "profile_city_and_state_updated", + str(profile.get("city") or "").casefold() == CITY.casefold() + and str(profile.get("state") or "").upper() == STATE, + f"expected_city={CITY!r}, expected_state={STATE!r}, " + f"after_city={profile.get('city')!r}, after_state={profile.get('state')!r}", + ) + david_id = user_id_for_email(initial_db, EMAIL) + judge.check( + "other_users_unchanged", + david_id is not None and rows_unchanged_except(initial_db, after_db, "users", [david_id]), + f"excluded_user_id={david_id!r}", + ) + from verify_lib import table_delta + delta = table_delta(initial_db, after_db, "users") + exact = False + if len(delta["changed"]) == 1 and not delta["added"] and not delta["removed"]: + before_row, after_row = delta["changed"][0] + exact = ( + before_row[0] == david_id == after_row[0] + and before_row[:7] == after_row[:7] + and before_row[9:] == after_row[9:] + and str(after_row[7]).casefold() == CITY.casefold() + and str(after_row[8]).upper() == STATE + ) + judge.check("profile_exact_delta", exact, f"delta={delta!r}") + check_tables_unchanged(judge, initial_db, after_db, ("saved_jobs", "applications")) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 — any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/walmart_careers/verify/verify_16.py b/sites/walmart_careers/verify/verify_16.py new file mode 100644 index 00000000..da4a9c6f --- /dev/null +++ b/sites/walmart_careers/verify/verify_16.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Verify Walmart Careers--16: Puerto Rico location + Weekday Day shift (+ Hourly rate); Cashier posting with the most open positions (read-only). + +Deterministic only: no LLM calls. Ground truth is hardcoded below and never +appears in tasks.jsonl. +""" +from __future__ import annotations + +import os +import re +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + Judge, + check_read_only, + check_results_visited, + check_trajectory_identity, + check_visited_job_detail, + contains_positions_count, + contains_req_id, + fail_closed, + final_answer, + load_run, + parse_args, + resolve_snapshots, +) + + +TASK_ID = "Walmart Careers--16" +JOB_ID = "CP-2503-10981" +POSITIONS = 5 +LOCATION_ALTERNATIVES = ("puerto rico", re.compile(r"^pr$")) + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + from ground_truth import constants_for_task + globals().update(constants_for_task(initial_db, int(TASK_ID.rsplit("--", 1)[1]))) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + check_results_visited( + judge, + trajectory, + "visited_results_required_filters", + {"q": "cashier", "loc": LOCATION_ALTERNATIVES, "shift": "Weekday Day", "rate": "Hourly"}, + ) + from ground_truth import task_ground_truth + for candidate in task_ground_truth(initial_db, 16)["candidates"]: + check_visited_job_detail(judge, trajectory, candidate["job_id"]) + judge.check( + "answer_has_requisition_id", + contains_req_id(answer, JOB_ID), + f"expected={JOB_ID!r}, answer={answer!r}", + ) + judge.check( + "answer_has_positions_count", + contains_positions_count(answer, POSITIONS), + f"expected={POSITIONS!r}, answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 — any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/walmart_careers/verify/verify_17.py b/sites/walmart_careers/verify/verify_17.py new file mode 100644 index 00000000..4af19208 --- /dev/null +++ b/sites/walmart_careers/verify/verify_17.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Verify Walmart Careers--17: Alice signs in, finds her single Part time saved role, applies with her account email; reports window + confirmation (stateful). + +Deterministic only: no LLM calls. Ground truth is hardcoded below and never +appears in tasks.jsonl. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + Judge, + application_rows, + check_signed_in_as, + check_trajectory_identity, + check_visited_job_detail, + check_visited_path, + contains_confirmation_number, + contains_shift_window, + fail_closed, + final_answer, + load_run, + new_application_rows, + parse_args, + resolve_snapshots, +) + + +TASK_ID = "Walmart Careers--17" +EMAIL = "alice.j@test.com" +USER_ID = 1 +JOB_ID = "CP-2503-11505" +SAVED_ROLES_PATH = "/candidate-home/saved-roles" +SHIFT_START = "6:00am" +SHIFT_END = "11:00am" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + from ground_truth import constants_for_task + globals().update(constants_for_task(initial_db, int(TASK_ID.rsplit("--", 1)[1]))) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + check_signed_in_as(judge, trajectory, EMAIL) + check_visited_path(judge, trajectory, "visited_saved_roles_page", SAVED_ROLES_PATH) + check_visited_job_detail(judge, trajectory, JOB_ID) + check_visited_path(judge, trajectory, "visited_apply_page", f"/jobs/{JOB_ID}/apply") + check_visited_path(judge, trajectory, "visited_apply_confirm_page", f"/jobs/{JOB_ID}/apply/confirm") + check_visited_path(judge, trajectory, "visited_apply_submitted_page", f"/jobs/{JOB_ID}/apply/submitted") + judge.check( + "initial_has_no_alice_application_for_target", + not application_rows(initial_db, job_id=JOB_ID, email=EMAIL), + f"job_id={JOB_ID}, email={EMAIL}", + ) + from verify_lib import check_paths_in_order + check_paths_in_order( + judge, trajectory, "workflow_in_order", + [("/login", {}), (SAVED_ROLES_PATH, {}), (f"/jobs/{JOB_ID}", {}), + (f"/jobs/{JOB_ID}/apply", {}), (f"/jobs/{JOB_ID}/apply/confirm", {}), + (f"/jobs/{JOB_ID}/apply/submitted", {})], + ) + new_rows = new_application_rows(initial_db, after_db) + judge.check( + "exactly_one_new_application", + len(new_rows) == 1, + f"new_rows={new_rows!r}", + ) + row = new_rows[0] if len(new_rows) == 1 else {} + judge.check( + "new_application_is_for_part_time_saved_role", + row.get("job_id") == JOB_ID, + f"expected_job_id={JOB_ID!r}, row={row!r}", + ) + judge.check( + "new_application_belongs_to_alice", + row.get("user_id") == USER_ID and str(row.get("email") or "").casefold() == EMAIL, + f"expected_user_id={USER_ID}, expected_email={EMAIL!r}, row={row!r}", + ) + judge.check( + "answer_has_shift_window", + contains_shift_window(answer, SHIFT_START, SHIFT_END), + f"expected={SHIFT_START!r}-{SHIFT_END!r}, answer={answer!r}", + ) + confirmation = str(row.get("confirmation_no") or "") + judge.check( + "answer_has_matching_confirmation_number", + bool(confirmation) and contains_confirmation_number(answer, confirmation), + f"row_confirmation_no={confirmation!r}, answer={answer!r}", + ) + from verify_lib import check_tables_unchanged, table_delta + delta = table_delta(initial_db, after_db, "applications") + judge.check( + "applications_exact_delta", + len(delta["added"]) == 1 and not delta["removed"] and not delta["changed"], + f"delta={delta!r}", + ) + check_tables_unchanged(judge, initial_db, after_db, ("users", "saved_jobs")) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 — any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/walmart_careers/verify/verify_18.py b/sites/walmart_careers/verify/verify_18.py new file mode 100644 index 00000000..074b5382 --- /dev/null +++ b/sites/walmart_careers/verify/verify_18.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +"""Verify Walmart Careers--18: Supply Chain area -> Drivers; Class A CDL Truck Driver Ottawa vs Williamsburg: more positions, ID + window + count (read-only). + +Deterministic only: no LLM calls. Ground truth is hardcoded below and never +appears in tasks.jsonl. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + Judge, + check_read_only, + check_results_visited, + check_trajectory_identity, + check_visited_job_detail, + check_visited_path, + contains_positions_count, + contains_req_id, + contains_shift_window, + fail_closed, + final_answer, + load_run, + parse_args, + resolve_snapshots, +) + + +TASK_ID = "Walmart Careers--18" +JOB_ID = "CP-6014-13829" +LOSER_ID = "CP-6088-11595" +AREA_PATH = "/careers-areas/supply-chain-and-transportation" +SHIFT_START = "5:00am" +SHIFT_END = "10:00am" +POSITIONS = 4 + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + from ground_truth import constants_for_task + globals().update(constants_for_task(initial_db, int(TASK_ID.rsplit("--", 1)[1]))) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + check_visited_path(judge, trajectory, "visited_supply_chain_area_page", AREA_PATH) + check_results_visited(judge, trajectory, "visited_drivers_category_results", {"category": "drivers"}) + check_visited_job_detail(judge, trajectory, JOB_ID) + check_visited_job_detail(judge, trajectory, LOSER_ID) + from verify_lib import check_paths_in_order + check_paths_in_order( + judge, trajectory, "winner_workflow_in_order", + [(AREA_PATH, {}), ("/results", {"category": "drivers"}), (f"/jobs/{JOB_ID}", {})], + ) + check_paths_in_order( + judge, trajectory, "comparison_workflow_in_order", + [(AREA_PATH, {}), ("/results", {"category": "drivers"}), (f"/jobs/{LOSER_ID}", {})], + ) + judge.check( + "answer_has_requisition_id", + contains_req_id(answer, JOB_ID), + f"expected={JOB_ID!r}, answer={answer!r}", + ) + judge.check( + "answer_has_shift_window", + contains_shift_window(answer, SHIFT_START, SHIFT_END), + f"expected={SHIFT_START!r}-{SHIFT_END!r}, answer={answer!r}", + ) + judge.check( + "answer_has_positions_count", + contains_positions_count(answer, POSITIONS), + f"expected={POSITIONS!r}, answer={answer!r}", + ) + judge.check( + "answer_does_not_report_other_posting_instead", + not (contains_req_id(answer, LOSER_ID) and not contains_req_id(answer, JOB_ID)), + f"winner={JOB_ID!r}, loser={LOSER_ID!r}, answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 — any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/walmart_careers/verify/verify_19.py b/sites/walmart_careers/verify/verify_19.py new file mode 100644 index 00000000..d4e1e899 --- /dev/null +++ b/sites/walmart_careers/verify/verify_19.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Verify Walmart Careers--19: Bob signs in; Stores and Clubs -> Digital Pickup and Delivery, Full time; fewest positions; save + report ID and street (stateful). + +Deterministic only: no LLM calls. Ground truth is hardcoded below and never +appears in tasks.jsonl. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + Judge, + check_results_visited, + check_signed_in_as, + check_tables_unchanged, + check_trajectory_identity, + check_visited_job_detail, + check_visited_path, + contains_req_id, + contains_street, + fail_closed, + final_answer, + load_run, + parse_args, + resolve_snapshots, + saved_job_ids, +) + + +TASK_ID = "Walmart Careers--19" +EMAIL = "bob.c@test.com" +JOB_ID = "CP-1179-11202" +STREET = "1301 SW Wanamaker Rd" +AREA_PATH = "/careers-areas/stores-and-clubs" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + from ground_truth import constants_for_task + globals().update(constants_for_task(initial_db, int(TASK_ID.rsplit("--", 1)[1]))) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + check_signed_in_as(judge, trajectory, EMAIL) + check_visited_path(judge, trajectory, "visited_stores_and_clubs_area_page", AREA_PATH) + check_results_visited( + judge, + trajectory, + "visited_digital_pickup_full_time_results", + {"category": "digital-pickup-and-delivery", "type": "Full time"}, + ) + from ground_truth import task_ground_truth + for candidate in task_ground_truth(initial_db, 19)["candidates"]: + check_visited_job_detail(judge, trajectory, candidate["job_id"]) + from verify_lib import check_paths_in_order + check_paths_in_order( + judge, trajectory, "workflow_in_order", + [("/login", {}), (AREA_PATH, {}), + ("/results", {"category": "digital-pickup-and-delivery", "type": "Full time"}), + (f"/jobs/{JOB_ID}", {})], + ) + judge.check( + "answer_has_requisition_id", + contains_req_id(answer, JOB_ID), + f"expected={JOB_ID!r}, answer={answer!r}", + ) + judge.check( + "answer_has_street_address", + contains_street(answer, STREET), + f"expected={STREET!r}, answer={answer!r}", + ) + before = saved_job_ids(initial_db, EMAIL) + after = saved_job_ids(after_db, EMAIL) + judge.check( + "initial_target_not_saved", + before is not None and JOB_ID not in before, + f"initial_saved={sorted(before or set())!r}, target={JOB_ID}", + ) + judge.check( + "target_saved_for_bob", + after is not None and JOB_ID in after, + f"email={EMAIL}, job_id={JOB_ID}, after_saved={sorted(after or set())!r}", + ) + judge.check( + "bob_saved_roles_changed_only_by_target", + before is not None and after == before | {JOB_ID}, + f"initial_saved={sorted(before or set())!r}, after_saved={sorted(after or set())!r}", + ) + from verify_lib import table_delta, user_id_for_email + delta = table_delta(initial_db, after_db, "saved_jobs") + bob_id = user_id_for_email(initial_db, EMAIL) + judge.check( + "saved_jobs_exact_delta", + len(delta["added"]) == 1 and not delta["removed"] and not delta["changed"] + and delta["added"][0][1:3] == (bob_id, JOB_ID), + f"delta={delta!r}", + ) + check_tables_unchanged(judge, initial_db, after_db, ("users", "applications")) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 — any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/walmart_careers/verify/verify_2.py b/sites/walmart_careers/verify/verify_2.py new file mode 100644 index 00000000..bd832490 --- /dev/null +++ b/sites/walmart_careers/verify/verify_2.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Verify Walmart Careers--2: Freight Handler at eComm Whse Logistics #9054 (Porterville): shift window + open positions (read-only). + +Deterministic only: no LLM calls. Ground truth is hardcoded below and never +appears in tasks.jsonl. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + Judge, + check_read_only, + check_trajectory_identity, + check_visited_job_detail, + contains_positions_count, + contains_shift_window, + fail_closed, + final_answer, + load_run, + parse_args, + resolve_snapshots, +) + + +TASK_ID = "Walmart Careers--2" +JOB_ID = "CP-9054-10921" +SHIFT_START = "6:00pm" +SHIFT_END = "3:00am" +POSITIONS = 3 + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + from ground_truth import constants_for_task + globals().update(constants_for_task(initial_db, int(TASK_ID.rsplit("--", 1)[1]))) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + check_visited_job_detail(judge, trajectory, JOB_ID) + judge.check( + "answer_has_shift_window", + contains_shift_window(answer, SHIFT_START, SHIFT_END), + f"expected={SHIFT_START!r}-{SHIFT_END!r}, answer={answer!r}", + ) + judge.check( + "answer_has_positions_count", + contains_positions_count(answer, POSITIONS), + f"expected={POSITIONS!r}, answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 — any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/walmart_careers/verify/verify_3.py b/sites/walmart_careers/verify/verify_3.py new file mode 100644 index 00000000..c0d54c19 --- /dev/null +++ b/sites/walmart_careers/verify/verify_3.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +"""Verify Walmart Careers--3: Healthcare area -> Pharmacy Services -> Bentonville Pharmacy Technician: hashtag + open positions (read-only). + +Deterministic only: no LLM calls. Ground truth is hardcoded below and never +appears in tasks.jsonl. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + Judge, + check_read_only, + check_results_visited, + check_trajectory_identity, + check_visited_job_detail, + check_visited_path, + contains_positions_count, + contains_hashtag, + fail_closed, + final_answer, + load_run, + parse_args, + resolve_snapshots, +) + + +TASK_ID = "Walmart Careers--3" +JOB_ID = "CP-5260-11531" +AREA_PATH = "/careers-areas/healthcare" +HASHTAG = "#pharmacytechjobs" +POSITIONS = 2 + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + from ground_truth import constants_for_task + globals().update(constants_for_task(initial_db, int(TASK_ID.rsplit("--", 1)[1]))) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + check_visited_path(judge, trajectory, "visited_healthcare_area_page", AREA_PATH) + check_results_visited( + judge, + trajectory, + "visited_healthcare_results", + {"category": "pharmacy-services"}, + {"area": "healthcare"}, + ) + check_visited_job_detail(judge, trajectory, JOB_ID) + from verify_lib import check_paths_in_order + check_paths_in_order( + judge, trajectory, "workflow_in_order", + [(AREA_PATH, {}), ("/results", {"area": "healthcare"}), (f"/jobs/{JOB_ID}", {})], + ) + judge.check( + "answer_has_hashtag", + contains_hashtag(answer, HASHTAG), + f"expected={HASHTAG!r}, answer={answer!r}", + ) + judge.check( + "answer_has_positions_count", + contains_positions_count(answer, POSITIONS), + f"expected={POSITIONS!r}, answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 — any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/walmart_careers/verify/verify_4.py b/sites/walmart_careers/verify/verify_4.py new file mode 100644 index 00000000..fefd9553 --- /dev/null +++ b/sites/walmart_careers/verify/verify_4.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""Verify Walmart Careers--4: Sam's Club / Part time / Weekend Overnight filters; Texas posting <= $20/hr: requisition ID + open positions (read-only). + +Deterministic only: no LLM calls. Ground truth is hardcoded below and never +appears in tasks.jsonl. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + Judge, + check_read_only, + check_results_visited, + check_trajectory_identity, + check_visited_job_detail, + contains_positions_count, + contains_req_id, + fail_closed, + final_answer, + load_run, + parse_args, + resolve_snapshots, +) + + +TASK_ID = "Walmart Careers--4" +JOB_ID = "CP-4750-11130" +POSITIONS = 3 + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + from ground_truth import constants_for_task + globals().update(constants_for_task(initial_db, int(TASK_ID.rsplit("--", 1)[1]))) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + check_results_visited( + judge, + trajectory, + "visited_results_required_filters", + {"brand": "Sam's Club", "type": "Part time", "shift": "Weekend Overnight"}, + ) + check_visited_job_detail(judge, trajectory, JOB_ID) + judge.check( + "answer_has_requisition_id", + contains_req_id(answer, JOB_ID), + f"expected={JOB_ID!r}, answer={answer!r}", + ) + judge.check( + "answer_has_positions_count", + contains_positions_count(answer, POSITIONS), + f"expected={POSITIONS!r}, answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 — any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/walmart_careers/verify/verify_5.py b/sites/walmart_careers/verify/verify_5.py new file mode 100644 index 00000000..e7d6ef25 --- /dev/null +++ b/sites/walmart_careers/verify/verify_5.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""Verify Walmart Careers--5: Full time Technology roles in Hoboken > $200k: requisition ID + Option 1 degree (read-only). + +Deterministic only: no LLM calls. Ground truth is hardcoded below and never +appears in tasks.jsonl. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + Judge, + check_read_only, + check_results_visited, + check_trajectory_identity, + check_visited_job_detail, + contains_all, + contains_req_id, + fail_closed, + final_answer, + load_run, + parse_args, + resolve_snapshots, +) + + +TASK_ID = "Walmart Careers--5" +JOB_ID = "R-2411489" +DEGREE = "bachelor" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + from ground_truth import constants_for_task + globals().update(constants_for_task(initial_db, int(TASK_ID.rsplit("--", 1)[1]))) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + check_results_visited( + judge, + trajectory, + "visited_results_required_filters", + {"area": "technology", "type": "Full time", "loc": "hoboken"}, + ) + check_visited_job_detail(judge, trajectory, JOB_ID) + judge.check( + "answer_has_requisition_id", + contains_req_id(answer, JOB_ID), + f"expected={JOB_ID!r}, answer={answer!r}", + ) + judge.check( + "answer_names_option_1_degree", + contains_all(answer, [DEGREE]), + f"expected_degree={DEGREE!r}, answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 — any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/walmart_careers/verify/verify_6.py b/sites/walmart_careers/verify/verify_6.py new file mode 100644 index 00000000..f8440778 --- /dev/null +++ b/sites/walmart_careers/verify/verify_6.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Verify Walmart Careers--6: Cleveland, OH location; Online Order Filling Team Supervisor: street + open positions (read-only). + +Deterministic only: no LLM calls. Ground truth is hardcoded below and never +appears in tasks.jsonl. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + Judge, + check_read_only, + check_results_visited, + check_trajectory_identity, + check_visited_job_detail, + contains_positions_count, + contains_street, + fail_closed, + final_answer, + load_run, + parse_args, + resolve_snapshots, +) + + +TASK_ID = "Walmart Careers--6" +JOB_ID = "CP-2073-11104" +STREET = "10000 Brookpark Rd" +POSITIONS = 2 + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + from ground_truth import constants_for_task + globals().update(constants_for_task(initial_db, int(TASK_ID.rsplit("--", 1)[1]))) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + check_results_visited( + judge, + trajectory, + "visited_results_required_filters", + {"loc": "cleveland", "radius": "25", "type": "Full time", "shift": "Weekday Day"}, + ) + check_visited_job_detail(judge, trajectory, JOB_ID) + judge.check( + "answer_has_street_address", + contains_street(answer, STREET), + f"expected={STREET!r}, answer={answer!r}", + ) + judge.check( + "answer_has_positions_count", + contains_positions_count(answer, POSITIONS), + f"expected={POSITIONS!r}, answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 — any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/walmart_careers/verify/verify_7.py b/sites/walmart_careers/verify/verify_7.py new file mode 100644 index 00000000..11336946 --- /dev/null +++ b/sites/walmart_careers/verify/verify_7.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Verify Walmart Careers--7: Students / Intern / Sam's Club filters; Bentonville merchandising internship: worker-type chip + street (read-only). + +Deterministic only: no LLM calls. Ground truth is hardcoded below and never +appears in tasks.jsonl. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + Judge, + check_read_only, + check_results_visited, + check_trajectory_identity, + check_visited_job_detail, + contains_all, + contains_street, + fail_closed, + final_answer, + load_run, + parse_args, + resolve_snapshots, +) + + +TASK_ID = "Walmart Careers--7" +JOB_ID = "R-2447168" +WORKER_TYPE_FRAGMENTS = ("intern", "fixed term") +STREET = "2101 SE Simple Savings Dr" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + from ground_truth import constants_for_task + globals().update(constants_for_task(initial_db, int(TASK_ID.rsplit("--", 1)[1]))) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + check_results_visited( + judge, trajectory, "visited_results_required_filters", + {"area": "students", "type": "Intern", "brand": "Sam's Club"}, + ) + check_visited_job_detail(judge, trajectory, JOB_ID) + judge.check( + "answer_has_worker_type_chip", + contains_all(answer, WORKER_TYPE_FRAGMENTS), + f"expected_fragments={WORKER_TYPE_FRAGMENTS!r}, answer={answer!r}", + ) + judge.check( + "answer_has_street_address", + contains_street(answer, STREET), + f"expected={STREET!r}, answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 — any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/walmart_careers/verify/verify_8.py b/sites/walmart_careers/verify/verify_8.py new file mode 100644 index 00000000..ebd88512 --- /dev/null +++ b/sites/walmart_careers/verify/verify_8.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Verify Walmart Careers--8: Two Mississippi Auto Care Center Technician postings: store number with more open positions + that count (read-only). + +Deterministic only: no LLM calls. Ground truth is hardcoded below and never +appears in tasks.jsonl. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + Judge, + check_read_only, + check_trajectory_identity, + check_visited_job_detail, + contains_positions_count, + fail_closed, + final_answer, + load_run, + mentions_store_number, + parse_args, + resolve_snapshots, +) + + +TASK_ID = "Walmart Careers--8" +WINNER_ID = "CP-1230-11592" +LOSER_ID = "CP-954-10637" +WINNER_STORE = 1230 +LOSER_STORE = 954 +POSITIONS = 5 + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + from ground_truth import constants_for_task + globals().update(constants_for_task(initial_db, int(TASK_ID.rsplit("--", 1)[1]))) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + check_visited_job_detail(judge, trajectory, WINNER_ID) + check_visited_job_detail(judge, trajectory, LOSER_ID) + judge.check( + "answer_names_winning_store_number", + mentions_store_number(answer, WINNER_STORE), + f"expected_store={WINNER_STORE!r}, answer={answer!r}", + ) + judge.check( + "answer_has_positions_count", + contains_positions_count(answer, POSITIONS), + f"expected={POSITIONS!r}, answer={answer!r}", + ) + judge.check( + "answer_does_not_name_other_store_instead", + not (mentions_store_number(answer, LOSER_STORE) and not mentions_store_number(answer, WINNER_STORE)), + f"winner_store={WINNER_STORE!r}, loser_store={LOSER_STORE!r}, answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 — any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/walmart_careers/verify/verify_9.py b/sites/walmart_careers/verify/verify_9.py new file mode 100644 index 00000000..68214b7e --- /dev/null +++ b/sites/walmart_careers/verify/verify_9.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Verify Walmart Careers--9: Two Marcy, NY Freight Handler postings: the earlier shift start, its requisition ID + window (read-only). + +Deterministic only: no LLM calls. Ground truth is hardcoded below and never +appears in tasks.jsonl. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + Judge, + check_read_only, + check_trajectory_identity, + check_visited_job_detail, + contains_req_id, + contains_shift_window, + fail_closed, + final_answer, + load_run, + parse_args, + resolve_snapshots, +) + + +TASK_ID = "Walmart Careers--9" +JOB_ID = "CP-6038-10642" +LOSER_ID = "CP-9046-10913" +SHIFT_START = "3:00pm" +SHIFT_END = "7:30pm" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + from ground_truth import constants_for_task + globals().update(constants_for_task(initial_db, int(TASK_ID.rsplit("--", 1)[1]))) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + check_visited_job_detail(judge, trajectory, JOB_ID) + check_visited_job_detail(judge, trajectory, LOSER_ID) + judge.check( + "answer_has_requisition_id", + contains_req_id(answer, JOB_ID), + f"expected={JOB_ID!r}, answer={answer!r}", + ) + judge.check( + "answer_has_shift_window", + contains_shift_window(answer, SHIFT_START, SHIFT_END), + f"expected={SHIFT_START!r}-{SHIFT_END!r}, answer={answer!r}", + ) + judge.check( + "answer_does_not_report_other_posting_instead", + not (contains_req_id(answer, LOSER_ID) and not contains_req_id(answer, JOB_ID)), + f"winner={JOB_ID!r}, loser={LOSER_ID!r}, answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 — any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/walmart_careers/verify/verify_lib.py b/sites/walmart_careers/verify/verify_lib.py new file mode 100644 index 00000000..24345cce --- /dev/null +++ b/sites/walmart_careers/verify/verify_lib.py @@ -0,0 +1,937 @@ +#!/usr/bin/env python3 +"""Shared deterministic helpers for Walmart Careers task verifiers. + +Each verifier consumes an agent run directory plus before/after SQLite snapshots +and emits ``{task_id, pass, reason, evidence[]}`` with exit code 0/1. + +No helper in this module calls an LLM; a verdict never depends on a key or a +model. Ground truth lives only inside the per-task ``verify_N.py`` files. +""" +from __future__ import annotations + +import argparse +import atexit +import hashlib +import ipaddress +import json +import os +import re +import sqlite3 +import subprocess +import tempfile +import unicodedata +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Iterable, Sequence +from urllib.parse import parse_qs, urlparse + +from PIL import Image + + +SITE = "walmart_careers" +DEFAULT_CONTAINER = os.environ.get("WH_CONTAINER", "wh-review") + +# Tables a read-only task must leave byte-for-row identical. +READ_ONLY_TABLES = ("users", "saved_jobs", "applications") + + +# --------------------------------------------------------------------------- # +# CLI / run loading +# --------------------------------------------------------------------------- # +@dataclass(frozen=True) +class VerifyArgs: + run_dir: str + initial_db: str | None + after_db: str | None + container: str + no_llm: bool + + +def parse_args() -> VerifyArgs: + parser = argparse.ArgumentParser() + parser.add_argument("--run_dir", required=True) + parser.add_argument("--initial_db") + parser.add_argument("--after_db") + parser.add_argument("--container", default=DEFAULT_CONTAINER) + parser.add_argument("--no_llm", action="store_true") + args = parser.parse_args() + run_dir = Path(args.run_dir) + initial_snapshot = run_dir / "initial.db" + after_snapshot = run_dir / "after.db" + return VerifyArgs( + run_dir=args.run_dir, + initial_db=( + args.initial_db + or (str(initial_snapshot) if initial_snapshot.is_file() else None) + ), + after_db=( + args.after_db or (str(after_snapshot) if after_snapshot.is_file() else None) + ), + container=args.container, + no_llm=args.no_llm, + ) + + +def load_run(run_dir: str | os.PathLike[str]) -> dict[str, Any]: + path = Path(run_dir) / "trajectory.json" + data = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(data, dict): + raise ValueError("trajectory.json must contain a JSON object") + data["_run_dir"] = str(Path(run_dir).resolve()) + return data + + +def final_answer(trajectory: dict[str, Any]) -> str: + return str(trajectory.get("final_answer") or "").strip() + + +def final_url(trajectory: dict[str, Any]) -> str: + direct = trajectory.get("final_url") + if direct: + return str(direct) + for step in reversed(trajectory.get("steps") or []): + if isinstance(step, dict) and step.get("url"): + return str(step["url"]) + return "" + + +def trajectory_urls(trajectory: dict[str, Any]) -> list[str]: + """Return every browser URL recorded by supported trajectory producers.""" + urls: list[str] = [] + if trajectory.get("start_url"): + urls.append(str(trajectory["start_url"])) + for step in trajectory.get("steps") or []: + if not isinstance(step, dict): + continue + for key in ("url", "url_before", "url_after"): + value = step.get(key) + if value: + urls.append(str(value)) + if trajectory.get("final_url"): + urls.append(str(trajectory["final_url"])) + return urls + + +def normalized_url_path(url: str) -> str: + path = urlparse(str(url or "")).path or "/" + return path.rstrip("/") or "/" + + +def is_walmart_careers_site_url(url: str) -> bool: + """Accept HTTP(S) URLs on a loopback host while allowing any port. + + Runs hit the alt-port container (41023) while tasks.jsonl says 40023, so + the port is deliberately not checked. + """ + parsed = urlparse(str(url or "")) + if parsed.scheme not in {"http", "https"} or not parsed.hostname: + return False + hostname = parsed.hostname.casefold() + if hostname == "localhost": + return True + try: + return ipaddress.ip_address(hostname).is_loopback + except ValueError: + return False + + +def site_urls(trajectory: dict[str, Any]) -> list[str]: + return [url for url in trajectory_urls(trajectory) if is_walmart_careers_site_url(url)] + + +def navigated_to_path(trajectory: dict[str, Any], expected_path: str) -> bool: + """Require an exact mirror path on a loopback origin, allowing any port.""" + expected = normalized_url_path(expected_path) + return any(normalized_url_path(url) == expected for url in site_urls(trajectory)) + + +def final_url_is_path(trajectory: dict[str, Any], expected_path: str) -> bool: + observed_url = final_url(trajectory) + return is_walmart_careers_site_url(observed_url) and normalized_url_path( + observed_url + ) == normalized_url_path(expected_path) + + +def trajectory_task_matches(trajectory: dict[str, Any], task_id: str) -> bool: + return str(trajectory.get("task_id") or "").strip() == task_id + + +def trajectory_input_texts(trajectory: dict[str, Any]) -> list[str]: + values: list[str] = [] + for step in trajectory.get("steps") or []: + if not isinstance(step, dict) or normalize_text(step.get("action")) != "input": + continue + params = step.get("params") + if isinstance(params, dict) and params.get("text") is not None: + values.append(str(params["text"])) + return values + + +def trajectory_input_contains(trajectory: dict[str, Any], expected_text: str) -> bool: + expected = normalize_text(expected_text) + return any(normalize_text(value) == expected for value in trajectory_input_texts(trajectory)) + + +def trajectory_last_email(trajectory: dict[str, Any]) -> str: + emails = [ + normalize_text(value) + for value in trajectory_input_texts(trajectory) + if re.fullmatch(r"[^@\s]+@[^@\s]+\.[^@\s]+", value.strip()) + ] + return emails[-1] if emails else "" + + +# --------------------------------------------------------------------------- # +# Results-page gates +# --------------------------------------------------------------------------- # +TEXT_PARAMS = {"q", "searchQuery", "loc"} +FACET_PARAMS = {"area", "category", "brand", "shift", "type", "rate"} + + +def results_visited(trajectory: dict[str, Any], **params: Any) -> bool: + """Some ``/results`` URL in the trajectory carries every requested param. + + ``q`` / ``searchQuery`` / ``loc`` are compared as normalized substrings of + the recorded value (``q`` also accepts the ``searchQuery`` alias). Facet + keys (``area``, ``category``, ``brand``, ``shift``, ``type``, ``rate``) must + appear as exact values in the parsed ``getlist``. An expected value may be a + string, a compiled regex (searched against the normalized value) or a + tuple/list of alternatives, any of which satisfies the key. + """ + for url in site_urls(trajectory): + if normalized_url_path(url) != "/results": + continue + query = parse_qs(urlparse(url).query, keep_blank_values=True) + if all(_param_matches(query, key, expected) for key, expected in params.items()): + return True + return False + + +def _param_matches(query: dict[str, list[str]], key: str, expected: Any) -> bool: + if isinstance(expected, (tuple, list, set, frozenset)): + return any(_param_matches(query, key, alt) for alt in expected) + values = list(query.get(key) or []) + if key == "q": + values += query.get("searchQuery") or [] + elif key == "searchQuery": + values += query.get("q") or [] + if key in TEXT_PARAMS: + if isinstance(expected, re.Pattern): + return any(expected.search(normalize_text(v)) for v in values) + expected_text = normalize_text(expected) + if key == "loc": + return any( + normalize_text(value) == expected_text + or normalize_text(value).startswith(expected_text + ",") + for value in values if value + ) + expected_tokens = set(re.findall(r"[a-z0-9]+", expected_text)) + return bool(expected_tokens) and any( + expected_tokens <= set(re.findall(r"[a-z0-9]+", normalize_text(value))) + for value in values if value + ) + if key not in FACET_PARAMS: + if isinstance(expected, re.Pattern): + return any(expected.fullmatch(normalize_text(value)) for value in values) + return any(normalize_text(expected) == normalize_text(value) for value in values) + if isinstance(expected, re.Pattern): + return any(expected.search(v) for v in values) + return str(expected) in values + + +def job_detail_visited(trajectory: dict[str, Any], job_id: str) -> bool: + """Exact ``/jobs/`` visit (never ``/jobs//apply``).""" + return navigated_to_path(trajectory, f"/jobs/{job_id}") + + +# --------------------------------------------------------------------------- # +# Text normalization and answer matchers +# --------------------------------------------------------------------------- # +def normalize_text(value: Any) -> str: + text = unicodedata.normalize("NFKC", str(value or "")) + text = text.replace("’", "'").replace("‘", "'").replace("“", '"').replace("”", '"') + return re.sub(r"\s+", " ", text).strip().casefold() + + +def _match_is_affirmative(text: str, match: re.Match[str]) -> bool: + before = re.split(r"[.!?;:\n]+|\b(?:but|however|instead)\b", text[:match.start()], flags=re.I)[-1] + after = text[match.end():] + return not re.search(r"\b(?:not|no|never|without|wrong|incorrect|isn't|wasn't|isnt|wasnt)\b", before, re.I) and not re.match( + r"\s*(?:is|was|are|were)?\s*(?:not|wrong|incorrect)\b", after, re.I + ) + + +def _affirmative_search(pattern: str, text: str, flags: int = 0) -> bool: + return any(_match_is_affirmative(text, match) for match in re.finditer(pattern, text, flags)) + + +def contains_all(text: Any, expected: Iterable[Any]) -> bool: + normalized = normalize_text(text) + return all( + bool(value_text) and _affirmative_search(re.escape(value_text), normalized) + for value_text in (normalize_text(value) for value in expected) + ) + + +def contains_any(text: Any, expected: Iterable[Any]) -> bool: + normalized = normalize_text(text) + return any( + bool(value_text) and _affirmative_search(re.escape(value_text), normalized) + for value_text in (normalize_text(value) for value in expected) + ) + + +DASH = r"[-‐‑‒–—−]" + + +def contains_req_id(text: Any, job_id: str) -> bool: + """Match a requisition ID such as ``CP-5991-12522`` or ``R-2468347``. + + Case-insensitive, tolerant of en/em dashes and spaces around the dashes, + anchored so ``CP-5991-125220`` or ``XCP-5991-12522`` do not count. + """ + raw = unicodedata.normalize("NFKC", str(text or "")) + parts = [re.escape(part) for part in str(job_id).split("-") if part] + pattern = r"(? bool: + normalized = normalize_text(text) + expected = normalize_text(tag) + if not expected.startswith("#"): + expected = "#" + expected + return _affirmative_search(r"(? tuple[int, int, str]: + match = re.fullmatch( + r"\s*(\d{1,2})(?::(\d{2}))?\s*([AaPp])\.?\s*[Mm]\.?\s*", str(value) + ) + if not match: + raise ValueError(f"unsupported clock time: {value!r}") + hour12 = int(match.group(1)) + minute = int(match.group(2) or 0) + meridiem = "am" if match.group(3).lower() == "a" else "pm" + if not 1 <= hour12 <= 12 or not 0 <= minute < 60: + raise ValueError(f"unsupported clock time: {value!r}") + return hour12, minute, meridiem + + +def _clock_pattern(value: str) -> str: + hour12, minute, meridiem = _parse_clock(value) + hour24 = hour12 % 12 + (12 if meridiem == "pm" else 0) + minutes = f":{minute:02d}" if minute else r"(?::00)?" + twelve_hour = rf"(? bool: + normalized = normalize_text(text) + return _affirmative_search(_clock_pattern(value), normalized) + + +def contains_shift_window(text: Any, start: str, end: str) -> bool: + """Both endpoints appear; ``6 pm`` / ``6:00 PM`` / ``6:00 p.m.`` / ``18:00`` all count.""" + return contains_clock_time(text, start) and contains_clock_time(text, end) + + +_ID_LIKE_MASKS = ( + # requisition IDs and confirmation numbers + rf"\b(?:CP|R|WMC)\s*{DASH}\s*\d+(?:\s*{DASH}\s*\d+)?", + # clock times + r"(? str: + masked = unicodedata.normalize("NFKC", str(text or "")) + for pattern in patterns: + masked = re.sub(pattern, " ~ ", masked, flags=re.I) + return masked + + +def _standalone_integer(text: str, number: int, allow_hash: bool = False) -> bool: + """``number`` as a whole integer: not part of a longer digit run, a decimal + (``2.5``), a thousands group (``2,000``) or an ordinal (``2nd``). A period + or comma that merely ends the sentence (``Open positions: 2.``) is fine — + real agents write the count that way (nano runs on tasks 3 and 4).""" + forbidden = r"[\d.,]" if allow_hash else r"[\d.,#]" + pattern = rf"(? bool: + """A bare integer equal to ``number`` after masking IDs, times, money, zips, + street numbers and store numbers; the word form (``three``) also counts.""" + masked = _mask(text, _ID_LIKE_MASKS + _STORE_MASKS) + if _standalone_integer(masked, int(number)): + return True + word = _NUMBER_WORDS.get(int(number)) + normalized = normalize_text(text) + return bool(word and _affirmative_search(rf"\b{word}\b", normalized)) + + +def contains_positions_count(text: Any, number: int) -> bool: + normalized = normalize_text(text) + word = _NUMBER_WORDS.get(int(number)) + values = [str(int(number))] + ([word] if word else []) + patterns = [] + for value in values: + escaped = re.escape(value) + patterns.extend([ + rf"(? bool: + normalized = normalize_text(text) + word = _NUMBER_WORDS.get(int(number)) + values = [str(int(number))] + ([word] if word else []) + return any( + _affirmative_search(rf"(? bool: + """``#1230``, ``store 1230`` or a standalone ``1230``, but not the ``1230`` + inside a requisition ID, a zip code or a street number.""" + masked = _mask(text, _ID_LIKE_MASKS) + return _standalone_integer(masked, int(number), allow_hash=True) + + +_DIRECTIONALS = { + "n": ("n", "north"), "s": ("s", "south"), "e": ("e", "east"), "w": ("w", "west"), + "ne": ("ne", "northeast"), "nw": ("nw", "northwest"), + "se": ("se", "southeast"), "sw": ("sw", "southwest"), +} +_SUFFIXES = { + "rd": ("rd", "road"), "st": ("st", "street"), "ave": ("ave", "avenue"), + "blvd": ("blvd", "boulevard"), "pkwy": ("pkwy", "parkway"), "dr": ("dr", "drive"), + "trl": ("trl", "trail"), "hwy": ("hwy", "highway"), "ln": ("ln", "lane"), + "ct": ("ct", "court"), "pl": ("pl", "place"), "cir": ("cir", "circle"), + "way": ("way",), +} +for _aliases in list(_DIRECTIONALS.values()): + for _alias in _aliases: + _DIRECTIONALS[_alias] = _aliases +for _aliases in list(_SUFFIXES.values()): + for _alias in _aliases: + _SUFFIXES[_alias] = _aliases + + +def contains_street(text: Any, street: str) -> bool: + """Number + core street tokens; suffix synonyms (``Rd``/``Road``, ...) and + directional synonyms (``S``/``South``, ``SE``/``Southeast``) are tolerated + and the directional/suffix tokens themselves are optional.""" + normalized = normalize_text(text) + tokens = normalize_text(street).replace(",", " ").split() + if not tokens: + return False + parts: list[str] = [] + for index, token in enumerate(tokens): + token = token.rstrip(".") + if token in _DIRECTIONALS: + alternatives = "|".join(re.escape(a) for a in _DIRECTIONALS[token]) + parts.append(rf"[\s,.]+(?:{alternatives})\b\.?") + elif token in _SUFFIXES: + alternatives = "|".join(re.escape(a) for a in _SUFFIXES[token]) + parts.append(rf"[\s,.]+(?:{alternatives})\b\.?") + elif re.fullmatch(r"[\d.]+", token): + separator = "" if index == 0 else r"[\s,.]+" + parts.append(rf"{separator}(? str: + return re.sub(r"\D", "", str(value or "")) + + +def extract_confirmation_numbers(text: Any) -> set[str]: + raw = unicodedata.normalize("NFKC", str(text or "")) + matches = re.findall(rf"(? bool: + expected = str(confirmation_no or "").upper() + raw = unicodedata.normalize("NFKC", str(text or "")) + parts = [re.escape(part) for part in expected.split("-") if part] + if not parts: + return False + return _affirmative_search(r"(? bool: + marker = "PASS" if condition else "FAIL" + self.evidence.append(f"[{marker}] {name}: {evidence}") + if not condition: + self.passed = False + if not self.reason: + self.reason = name + return condition + + def emit(self) -> None: + result = { + "task_id": self.task_id, + "pass": self.passed, + "reason": self.reason or "all checks passed", + "evidence": self.evidence, + } + print(json.dumps(result, ensure_ascii=False, indent=2)) + raise SystemExit(0 if self.passed else 1) + + +def fail_closed(task_id: str, reason: str, detail: str) -> None: + print( + json.dumps( + { + "task_id": task_id, + "pass": False, + "infra_error": True, + "reason": reason, + "evidence": [f"[FAIL] {reason}: {detail}"], + }, + ensure_ascii=False, + indent=2, + ) + ) + raise SystemExit(1) + + +def _same_local_origin(url: str, start_url: str) -> bool: + try: + observed = urlparse(str(url or "")) + start = urlparse(str(start_url or "")) + return ( + observed.scheme == start.scheme == "http" + and observed.hostname is not None + and start.hostname is not None + and not observed.username + and not observed.password + and observed.port == start.port + and observed.hostname.casefold() == start.hostname.casefold() + and is_walmart_careers_site_url(url) + ) + except ValueError: + return False + + +def _screenshots_decode(trajectory: dict[str, Any]) -> tuple[bool, str]: + root = Path(str(trajectory.get("_run_dir") or "")) + steps = trajectory.get("steps") + if not root.is_dir() or not isinstance(steps, list) or not steps: + return False, "run directory or steps are missing" + checked = 0 + for index, step in enumerate(steps): + if not isinstance(step, dict): + return False, f"step {index} is not an object" + for key in ("screenshot_before", "screenshot_after"): + name = step.get(key) + relative = Path(str(name or "")) + if not name or relative.is_absolute() or ".." in relative.parts: + return False, f"step {index} has unsafe {key}" + candidates = (root / "screenshots" / relative, root / relative) + path = next((item for item in candidates if item.is_file()), None) + if path is None: + return False, f"step {index} is missing {key}={name!r}" + try: + with Image.open(path) as image: + image.load() + if image.format != "PNG" or image.width < 1 or image.height < 1: + return False, f"step {index} {key} is not a nonempty PNG" + except Exception as exc: + return False, f"step {index} {key} cannot decode: {type(exc).__name__}" + checked += 1 + return True, f"decoded {checked} PNG screenshots" + + +def check_trajectory_identity(judge: Judge, trajectory: dict[str, Any], task_id: str) -> None: + judge.check( + "final_answer_nonempty", + bool(final_answer(trajectory)), + f"final_answer={final_answer(trajectory)!r}", + ) + judge.check( + "trajectory_task_matches", + trajectory_task_matches(trajectory, task_id), + f"expected_task_id={task_id!r}, observed_task_id={trajectory.get('task_id')!r}", + ) + steps = trajectory.get("steps") + judge.check( + "trajectory_completed", + trajectory.get("terminated") is True and trajectory.get("termination_reason") == "agent_done", + f"terminated={trajectory.get('terminated')!r}, reason={trajectory.get('termination_reason')!r}", + ) + judge.check("trajectory_has_steps", isinstance(steps, list) and bool(steps), f"steps={len(steps) if isinstance(steps, list) else 'invalid'}") + recorded = trajectory_urls(trajectory) + judge.check( + "all_urls_match_local_origin", + bool(recorded) and all(_same_local_origin(url, trajectory.get("start_url", "")) for url in recorded), + f"start_url={trajectory.get('start_url')!r}, recorded_urls={recorded!r}", + ) + screenshots_ok, screenshot_evidence = _screenshots_decode(trajectory) + judge.check("screenshots_decode", screenshots_ok, screenshot_evidence) + + +def check_signed_in_as(judge: Judge, trajectory: dict[str, Any], email: str) -> None: + judge.check( + "visited_login_page", + navigated_to_path(trajectory, "/login"), + "required_path=/login", + ) + judge.check( + "entered_expected_account_email", + trajectory_last_email(trajectory) == normalize_text(email), + f"expected_email={email!r}, last_entered_email={trajectory_last_email(trajectory)!r}", + ) + + +def check_visited_path(judge: Judge, trajectory: dict[str, Any], name: str, path: str) -> bool: + return judge.check(name, navigated_to_path(trajectory, path), f"required_path={path}") + + +def check_visited_job_detail(judge: Judge, trajectory: dict[str, Any], job_id: str) -> bool: + return judge.check( + f"visited_job_detail_{job_id}", + job_detail_visited(trajectory, job_id), + f"required_path=/jobs/{job_id}", + ) + + +def check_paths_in_order( + judge: Judge, + trajectory: dict[str, Any], + name: str, + requirements: Sequence[tuple[str, dict[str, Any]]], +) -> bool: + urls = site_urls(trajectory) + cursor = 0 + for expected_path, params in requirements: + expected = normalized_url_path(expected_path) + for index in range(cursor, len(urls)): + url = urls[index] + query = parse_qs(urlparse(url).query, keep_blank_values=True) + if normalized_url_path(url) == expected and all( + _param_matches(query, key, value) for key, value in params.items() + ): + cursor = index + 1 + break + else: + return judge.check(name, False, f"requirements={requirements!r}, observed={urls!r}") + return judge.check(name, True, f"requirements={requirements!r}") + + +def check_results_visited( + judge: Judge, trajectory: dict[str, Any], name: str, *alternatives: dict[str, Any] +) -> bool: + """PASS when any of the ``alternatives`` param sets matches a /results visit.""" + matched = any(results_visited(trajectory, **params) for params in alternatives) + described = " OR ".join(_describe_params(params) for params in alternatives) + results = [url for url in site_urls(trajectory) if normalized_url_path(url) == "/results"] + return judge.check( + name, matched, f"required=/results?{described}; observed_results_urls={results!r}" + ) + + +def _describe_params(params: dict[str, Any]) -> str: + pieces = [] + for key, value in params.items(): + if isinstance(value, re.Pattern): + value = f"/{value.pattern}/" + pieces.append(f"{key}~{value!r}") + return "&".join(pieces) + + +# --------------------------------------------------------------------------- # +# SQLite state +# --------------------------------------------------------------------------- # +def db_query( + db_path: str | os.PathLike[str], sql: str, params: Sequence[Any] = () +) -> list[sqlite3.Row]: + connection = sqlite3.connect(str(db_path)) + connection.row_factory = sqlite3.Row + try: + return connection.execute(sql, params).fetchall() + finally: + connection.close() + + +def fetch_db(container: str, kind: str) -> str: + if kind not in {"instance", "instance_seed"}: + raise ValueError(f"unsupported DB kind: {kind}") + handle, destination = tempfile.mkstemp(prefix=f"{SITE}_{kind}_", suffix=".db") + os.close(handle) + source = f"{container}:/opt/WebSyn/{SITE}/{kind}/{SITE}.db" + result = subprocess.run( + ["docker", "cp", source, destination], capture_output=True, text=True + ) + if result.returncode: + Path(destination).unlink(missing_ok=True) + detail = result.stderr.strip() or result.stdout.strip() + raise RuntimeError(f"could not copy {source}: {detail}") + atexit.register(Path(destination).unlink, missing_ok=True) + return destination + + +def resolve_db(explicit_path: str | None, container: str, kind: str) -> str | None: + if explicit_path: + path = Path(explicit_path) + return str(path) if path.is_file() else None + try: + return fetch_db(container, kind) + except (OSError, RuntimeError): + return None + + +EXPECTED_TABLES = {"application_drafts", "applications", "areas", "categories", "jobs", "saved_jobs", "seed_metadata", "stores", "users"} +IMMUTABLE_TABLES = ("areas", "categories", "stores", "jobs", "seed_metadata", "application_drafts") + + +def _schema_objects(db_path: str) -> list[tuple[Any, ...]]: + return [ + tuple(row) + for row in db_query( + db_path, + "SELECT type, name, tbl_name, sql FROM sqlite_schema " + "WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%' ORDER BY type, name", + ) + ] + + +def _validate_snapshot_contract(initial_db: str, after_db: str) -> None: + initial_tables = {row["name"] for row in db_query(initial_db, "SELECT name FROM sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%'")} + after_tables = {row["name"] for row in db_query(after_db, "SELECT name FROM sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%'")} + if initial_tables != EXPECTED_TABLES or after_tables != EXPECTED_TABLES: + raise ValueError(f"unexpected tables: initial={sorted(initial_tables)}, after={sorted(after_tables)}") + initial_schema = _schema_objects(initial_db) + if initial_schema != _schema_objects(after_db): + raise ValueError("initial and after database schemas differ") + schema_hash = hashlib.sha256(json.dumps(initial_schema, separators=(",", ":")).encode()).hexdigest() + if schema_hash != "6067cd253ea017c494c5b3efacccd6e8068b5ff40709f9c17f80b45f4a69bebf": + raise ValueError(f"unsupported Walmart Careers schema hash: {schema_hash}") + marker = db_query(initial_db, "SELECT value FROM seed_metadata WHERE key='version'") + if len(marker) != 1 or marker[0]["value"] != "walmart-careers-v2": + raise ValueError("initial database seed version is missing or unsupported") + expected_counts = {"areas": 7, "categories": 33, "stores": 51, "jobs": 246, "users": 4, "application_drafts": 0} + observed = {table: len(table_rows(initial_db, table)) for table in expected_counts} + if observed != expected_counts: + raise ValueError(f"initial database counts differ: expected={expected_counts}, observed={observed}") + changed = [table for table in IMMUTABLE_TABLES if table_rows(initial_db, table) != table_rows(after_db, table)] + if changed: + raise ValueError(f"immutable catalog tables changed: {changed}") + + +def resolve_snapshots(args: VerifyArgs, task_id: str) -> tuple[str, str]: + """Return validated (initial_db, after_db) snapshots or fail closed.""" + initial_db = resolve_db(args.initial_db, args.container, "instance_seed") + after_db = resolve_db(args.after_db, args.container, "instance") + if not initial_db or not after_db: + fail_closed( + task_id, + "database_unavailable", + "both initial and after walmart_careers database snapshots are required", + ) + try: + _validate_snapshot_contract(str(initial_db), str(after_db)) + from ground_truth import task_ground_truth + task_number = int(task_id.rsplit("--", 1)[1]) + task_ground_truth(str(initial_db), task_number) + except (ImportError, OSError, sqlite3.Error, ValueError) as exc: + fail_closed(task_id, "snapshot_contract_invalid", str(exc)) + return str(initial_db), str(after_db) + + +def user_id_for_email(db_path: str, email: str) -> int | None: + rows = db_query( + db_path, + "SELECT id FROM users WHERE lower(email) = lower(?) ORDER BY id LIMIT 1", + (email,), + ) + return int(rows[0]["id"]) if rows else None + + +def user_emails(db_path: str) -> set[str]: + rows = db_query(db_path, "SELECT email FROM users") + return {normalize_text(row["email"]) for row in rows if row["email"]} + + +def user_ids(db_path: str) -> set[int]: + return {int(row["id"]) for row in db_query(db_path, "SELECT id FROM users")} + + +def new_user_ids(initial_db: str, after_db: str) -> set[int]: + return user_ids(after_db) - user_ids(initial_db) + + +def new_user_emails(initial_db: str, after_db: str) -> set[str]: + fresh = new_user_ids(initial_db, after_db) + rows = db_query(after_db, "SELECT id, email FROM users") + return {normalize_text(row["email"]) for row in rows if int(row["id"]) in fresh and row["email"]} + + +def user_profile(db_path: str, email: str) -> dict[str, Any] | None: + rows = db_query( + db_path, + "SELECT id, email, first_name, last_name, phone, city, state, display_name " + "FROM users WHERE lower(email) = lower(?) ORDER BY id LIMIT 1", + (email,), + ) + return dict(rows[0]) if rows else None + + +def saved_job_ids_by_user_id(db_path: str, user_id: int) -> set[str]: + rows = db_query(db_path, "SELECT job_id FROM saved_jobs WHERE user_id = ?", (user_id,)) + return {str(row["job_id"]) for row in rows} + + +def saved_job_ids(db_path: str, email: str) -> set[str] | None: + user_id = user_id_for_email(db_path, email) + if user_id is None: + return None + return saved_job_ids_by_user_id(db_path, user_id) + + +def saved_jobs_delta(initial_db: str, after_db: str, email: str) -> tuple[set[str], set[str]]: + before = saved_job_ids(initial_db, email) or set() + after = saved_job_ids(after_db, email) or set() + return after - before, before - after + + +def application_rows( + db_path: str, + job_id: str | None = None, + email: str | None = None, + phone_digits: str | None = None, + user_id: int | None = None, +) -> list[dict[str, Any]]: + rows = db_query( + db_path, + "SELECT id, job_id, user_id, email, first_name, last_name, phone, status, " + "confirmation_no FROM applications ORDER BY id", + ) + selected: list[dict[str, Any]] = [] + for row in rows: + item = dict(row) + if job_id is not None and str(item["job_id"]) != job_id: + continue + if email is not None and normalize_text(item["email"]) != normalize_text(email): + continue + if phone_digits is not None and digits_only(item["phone"]) != digits_only(phone_digits): + continue + if user_id is not None and item["user_id"] != user_id: + continue + selected.append(item) + return selected + + +def new_application_rows(initial_db: str, after_db: str) -> list[dict[str, Any]]: + initial_ids = {int(row["id"]) for row in db_query(initial_db, "SELECT id FROM applications")} + return [row for row in application_rows(after_db) if int(row["id"]) not in initial_ids] + + +def table_rows(db_path: str, table: str) -> list[tuple[Any, ...]]: + if not re.fullmatch(r"[a-z_]+", table): + raise ValueError(f"unsupported table: {table}") + rows = db_query(db_path, f"SELECT * FROM {table} ORDER BY 1") + return [tuple(row) for row in rows] + + +def table_counts(db_path: str, tables: Iterable[str] = READ_ONLY_TABLES) -> dict[str, int]: + return {table: len(table_rows(db_path, table)) for table in tables} + + +def table_delta(initial_db: str, after_db: str, table: str) -> dict[str, list[Any]]: + before = {int(row[0]): row for row in table_rows(initial_db, table)} + after = {int(row[0]): row for row in table_rows(after_db, table)} + common = before.keys() & after.keys() + return { + "added": [after[key] for key in sorted(after.keys() - before.keys())], + "removed": [before[key] for key in sorted(before.keys() - after.keys())], + "changed": [(before[key], after[key]) for key in sorted(common) if before[key] != after[key]], + } + + +def tables_unchanged(initial_db: str, after_db: str, tables: Iterable[str]) -> dict[str, bool]: + return { + table: table_rows(initial_db, table) == table_rows(after_db, table) for table in tables + } + + +def rows_unchanged_except( + initial_db: str, after_db: str, table: str, excluded_ids: Iterable[int] +) -> bool: + excluded = {int(value) for value in excluded_ids} + before = [row for row in table_rows(initial_db, table) if int(row[0]) not in excluded] + after = [row for row in table_rows(after_db, table) if int(row[0]) not in excluded] + return before == after + + +def check_tables_unchanged( + judge: Judge, initial_db: str, after_db: str, tables: Iterable[str], prefix: str = "" +) -> None: + """One ``_unchanged`` check per table (e.g. ``applications_unchanged``).""" + for table, same in tables_unchanged(initial_db, after_db, tables).items(): + judge.check( + f"{prefix}{table}_unchanged", + same, + f"table={table}, initial_rows={len(table_rows(initial_db, table))}, " + f"after_rows={len(table_rows(after_db, table))}, identical={same}", + ) + + +def check_read_only(judge: Judge, initial_db: str, after_db: str) -> None: + """Read-only tasks: users, saved_jobs and applications must be row-identical.""" + check_tables_unchanged(judge, initial_db, after_db, READ_ONLY_TABLES, prefix="read_only_") diff --git a/websyn_start.sh b/websyn_start.sh index f6beda6e..9539b4f9 100644 --- a/websyn_start.sh +++ b/websyn_start.sh @@ -5,7 +5,7 @@ set -e SITES=(allrecipes amazon apple arxiv bbc_news booking github google_flights google_map google_search huggingface wolfram_alpha - cambridge_dictionary coursera espn merriam_webster ikea phys_org target ted osu rotten_tomatoes compass) + cambridge_dictionary coursera espn merriam_webster ikea phys_org target ted osu rotten_tomatoes compass walmart_careers) BASE_PORT=40000 PID_DIR=/tmp/websyn_pids mkdir -p "$PID_DIR" @@ -61,6 +61,7 @@ done # Final status report echo "[WebSyn] Site status:" +failed=0 for i in "${!SITES[@]}"; do site="${SITES[$i]}" port=$((BASE_PORT + i)) @@ -74,9 +75,20 @@ except Exception: exit(1) echo " [OK] $site :$port" else echo " [!!] $site :$port FAILED -- check /tmp/websyn_${site}.log" + failed=1 fi done +if [ "$failed" -ne 0 ]; then + echo "[WebSyn] Startup failed; stopping site supervisors." >&2 + for pid_file in "$PID_DIR"/*.pid; do + [ -f "$pid_file" ] || continue + pid=$(cat "$pid_file") + kill -KILL -- "-$pid" 2>/dev/null || true + done + exit 1 +fi + echo "[WebSyn] Starting control server on :8101 (PID 1)..." # Control server becomes PID 1 — receives SIGTERM on `docker stop`,