diff --git a/.env.production.example b/.env.production.example index c616601..f807c91 100644 --- a/.env.production.example +++ b/.env.production.example @@ -1,7 +1,7 @@ # Production deployments intentionally have no working secret defaults. # Copy this file to .env.production, replace every CHANGE-ME value, keep the # result out of version control, and pin a released OSApplyTrack version. -OSAPPLYTRACK_VERSION=1.66.5 +OSAPPLYTRACK_VERSION=1.66.6 POSTGRES_USER=applytrack POSTGRES_PASSWORD=CHANGE-ME-use-a-long-random-password POSTGRES_DB=applytrack diff --git a/BACKLOG.md b/BACKLOG.md index 95de901..a099068 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -165,6 +165,8 @@ review items and unsupported forms still need human attention. - [ ] [#408 — Agent: LinkedIn SDUI Easy Apply — the contact step's City typeahead is still left unmapped (SMX)](https://github.com/CryptoJones/OSApplyTrack/issues/408) (found re-running the Ready lane after #406: `CityLabel()` matches and the résumé location is typed, but the typeahead is never committed on the SDUI dialog; the run stops at the contact step with `City` the only unmapped field) - [ ] [#410 — Agent: a packet's whole answer draft rides one LLM completion under the API's 120 s timeout — Orbis's 29-question form timed out twice and every answer was voided](https://github.com/CryptoJones/OSApplyTrack/issues/410) (not the network or the endpoint — a 36 KB request from the agent's own netns answers in 1.5 s; the real 22-question prompt just takes poolside longer than two minutes. Mitigated on pluto with `Llm__TimeoutSeconds=600` on the agent; the fix is a worker-side ceiling, chunked drafting, or both) - [ ] [#409 — Agent: LinkedIn SDUI Easy Apply — Trility's dialog never reaches Submit within 12 steps](https://github.com/CryptoJones/OSApplyTrack/issues/409) (`mapped: 0, unmapped: []` — the driver presses Next twelve times on a step it cannot leave and reports nothing about it; needs the screenshot and the live dialog) +- [x] [#416 — Agent: two controls under one label — OneStream's address Country lost to the phone's country picker](https://github.com/CryptoJones/OSApplyTrack/issues/416) (fixed in 1.66.6: when a locator matches several controls, the one whose id or name is the question's own id wins; a react-select reads empty after a choice, so "prefer the empty one" took the phone's picker every time.) +- [x] [#414 — Agent: a consent question that mentions the phone number gets answered with the phone number (Prenuvo SMS opt-in)](https://github.com/CryptoJones/OSApplyTrack/issues/414) (fixed in 1.66.6: the drafter's phone and email rules skip a label that asks for consent, opt-in or agreement, or names SMS, text messages, WhatsApp or subscribing; plain "Phone" / "Mobile phone number" still take the number.) - [x] [#412 — Agent: LinkedIn SDUI years-of-experience text boxes refuse prose answers ("5+ years")](https://github.com/CryptoJones/OSApplyTrack/issues/412) (fixed in 1.66.5: the SDUI dialog draws "How many years …?" as a plain 20-character text box, so the #332 whole-number coercion keyed on `type=number` never fired and the drafter's "10+ years — …" was refused after Next. A label that asks "How many years" now gets the whole number its answer states, and "Less than 1 year" reads 0, not 1. Found on the 2026-10-05 Ready sweep: seven LinkedIn rows rebuilt on Claude answered every question and still dry-ran 0 mapped.) - [x] [#406 — Agent: Ready rows stuck on failures the worker should have recovered from — DNS blip stamped non-transient, browser crash unflagged, LLM 429 hammered every tick](https://github.com/CryptoJones/OSApplyTrack/issues/406) (fixed in 1.66.4: "the posting's host could not be resolved" is a `TransientRunException`, retried once after two seconds and recorded as transient, so the reconciler retries it; a run the browser died under ("Target page, context or browser has been closed") is flagged `transient` by the submitter and read by those words from before; and a pass stops judging at the first `LlmUnavailableException` and holds the tenant off for 15 minutes instead of asking the next eleven leads every five minutes. The eight rows the fix could not reach — four stored `transient: false`, launchdarkly's crash, and three LinkedIn runs from before #403 — were queued again by hand.) - [x] [#403 — Agent: LinkedIn Easy Apply times out on the new SDUI dialog (no .jobs-easy-apply-modal)](https://github.com/CryptoJones/OSApplyTrack/issues/403) (fixed in 1.66.2: the driver now also recognizes LinkedIn's SDUI ``. It finds Next / Review / Submit application and Dismiss by their text or label, reads fields from `label[for]` / `aria-label` and radio options from the text beside each one, catches errors that appear only after a refused Next, unticks Follow by its aria-label, and answers the blank mobile-number box from the packet's standard phone. Checked against the live dialog on 2026-10-03.) diff --git a/api/ApplyTrack.Api.Tests/AnswerDrafterTests.cs b/api/ApplyTrack.Api.Tests/AnswerDrafterTests.cs index f55a3b7..de7642f 100644 --- a/api/ApplyTrack.Api.Tests/AnswerDrafterTests.cs +++ b/api/ApplyTrack.Api.Tests/AnswerDrafterTests.cs @@ -545,4 +545,23 @@ public void Fit_to_options_matches_case_insensitively_and_splits_a_multiselect() var free = new PacketQuestion("q", "Why", true, PacketQuestion.Textarea, [], PacketQuestion.Custom); Assert.Equal(("because", null), AnswerDrafter.FitToOptions(free, "because")); } + + [Theory] + [InlineData("By selecting YES, I consent to receive recruiting SMS messages from Prenuvo at the phone number provided on my job application.")] + [InlineData("Do you opt-in to receive WhatsApp messages at your mobile number?")] + [InlineData("I agree to receive updates by email")] + public void A_consent_question_naming_a_phone_or_email_is_not_that_field(string label) + { + var (answer, _) = AnswerDrafter.Deterministic( + new("question_1", label, true, PacketQuestion.Select, ["Yes", "No"], PacketQuestion.Custom), Ctx()); + Assert.NotEqual("555-0100", answer); + Assert.False(answer is { } a && a.Contains('@')); + } + + [Theory] + [InlineData("Phone")] + [InlineData("Mobile phone number")] + public void A_plain_phone_label_still_takes_the_number(string label) => + Assert.Equal(("555-0100", null), AnswerDrafter.Deterministic( + new("question_2", label, true, PacketQuestion.Text, [], PacketQuestion.Custom), Ctx())); } diff --git a/api/ApplyTrack.Api.Tests/BrowserSubmitterTests.cs b/api/ApplyTrack.Api.Tests/BrowserSubmitterTests.cs index 8230bd5..76c0447 100644 --- a/api/ApplyTrack.Api.Tests/BrowserSubmitterTests.cs +++ b/api/ApplyTrack.Api.Tests/BrowserSubmitterTests.cs @@ -470,6 +470,7 @@ function suggest(box) { // Greenhouse's current form: react-select comboboxes for country, city (an async // autocomplete) and the custom questions, each with a hidden required input. _fixture.MapGet("/jobs/react-select", () => Results.Content(ReactSelectHtml, "text/html")); + _fixture.MapGet("/jobs/react-select-two-countries", () => Results.Content(ReactSelectTwoCountriesHtml, "text/html")); // A form whose own validation blocks Submit on a field the DOM never marks required. _fixture.MapGet("/jobs/validating", () => Results.Content(ValidatingFormHtml, "text/html")); // A form that re-mounts itself on Submit instead of posting. @@ -1134,6 +1135,27 @@ Click the object that does not fit the column pattern """; + // OneStream's form (#416): the phone number's country picker and the address block's + // Country are both labelled "Country*", the address one further down the page. + private static readonly string ReactSelectTwoCountriesHtml = ReactSelectHtml.Replace( + "