Repository navigation
feat(e2e): add contract details schema canary script - #1382
gabrielseco wants to merge 60 commits into
Conversation
Lets seed-onboarding.mjs bypass the local example dev server via --env=<name>,
reusing example/api/{utils,get_token,proxy}.js for auth against .env.<env> at
the repo root. Prints a ready-to-click app link (?employmentId= prefilled,
via VITE_APP_URL) when set. Onboarding.tsx now reads employmentId from the
URL query string so that link actually prefills the intro form.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Captures the manual sandbox-verification loop (seed via --env=sandbox, log past the Vercel password gate with VITE_APP_PASSWORD, drive the flow, check the claim) as a repeatable skill instead of re-deriving it each time. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Split into its own PR (#1379) so it can be reviewed independently of the seed-onboarding env flag. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The basic and Germany onboarding specs create real employments against the sandbox and ran against Playwright's 30s test / 5s expect defaults. Traces from PR #1371 and main show nothing hanging: Germany reached the final ILA sign click at 27.2s, leaving <3s for the sign POST, and Spain failed step-title assertions while ~3s employment writes were in flight. - test.slow() on both onboarding specs (90s budget) - expectOnboardingStep helper with a 15s timeout for step transitions, which are gated on sandbox writes rather than rendering - wait on the ILA sign POST and assert it succeeded before checking the dialog closed, so a backend rejection reports as such Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ally
Replaces the per-spec test.slow() and the 15s step-title timeout with:
- submitAndWaitForSave / clickAndWaitForSave helpers that wait for the
request each step's submit fires (POST /v1/employments, POST
engagement-agreement-details, PATCH /v1/employments/{id}, PUT
benefit-offers, POST .../sign) and assert it succeeded, instead of
waiting for the "Loading..." text to disappear. A backend failure now
reports method, path and status.
- Global timeout (60s) and expect timeout (10s) in playwright.config.ts,
matching Dragon's defaults. Response waits make failures precise but do
not make the sandbox faster; the Germany flow alone takes ~30s on a
good day, so the 30s default budget has to go up regardless.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Give an early signal when a live contract_details JSON schema served by tiger can no longer be turned into a form by remote-flows, for every country (not only the DEU/ESP pair covered by e2e today). This is the detection the France incident showed we were missing: tiger changed a schema in place instead of versioning it, and nothing in the suite exercised the real sandbox schema against the engine our customers run. Phase 1 only: checks the `main` engine (this repo's src/), both the pinned schema version and `latest`, across every /v1/countries country with eor_onboarding enabled. Reuses createHeadlessForm and usesJsfV1ContractDetails from src/flows/Onboarding so the check runs the same code path as the real Onboarding flow. A pinned-version mismatch fails the job; a latest-only failure is reported as a warning. Adds a daily schema-canary CI workflow and an empty, ready-to-fill skip list for known non-applicable countries/versions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
📦 Bundle Size Report
Size Limits
Largest Files (Top 5)
View All Files (291 total)
✅ Bundle size check passed |
|
Deploy preview for adp-cost-calculator ready!
Deployed with vercel-action |
|
Deploy preview for remote-flows ready!
Deployed with vercel-action |
Review follow-up: fetchLiveSchema was casting response.data.data to a non-nullable Record, silently throwing away the undefined case the generated CountryFormResponse type actually allows. Use ?? null to keep it honest with the function's own declared return type. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
📊 Coverage Report✅ Coverage increased! 🎉
Detailed BreakdownLines Coverage
Statements Coverage
Functions Coverage
Branches Coverage
✅ Coverage check passed |
…naming Remove --dry-run and its local-fixture data now that live sandbox credentials are available to exercise the script directly. Rename the sandbox client-credentials env vars from SANDBOX_CLIENT_ID/SECRET to VITE_CLIENT_ID/VITE_CLIENT_SECRET to match example/.env's nomenclature, and update the CI workflow's secret mapping to match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…n file jsonSchemaVersion / jsonSchemaVersionByCountry were inlined in Onboarding's constants.ts. Splitting them into jsonSchemaVersions.ts makes the versioning config importable from other example flows without pulling in the rest of the Onboarding options (jsfModify, features, etc). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per PR #1344's gap report, countries absent from jsonSchemaVersionByCountry silently fall back to contract_details v1. Add every country from COUNTRY_CONTRACT_VERSIONS explicitly (67 new entries at v1, matching current behavior), so the map is exhaustive and the v1 pin is visible instead of implicit. FRA_SAS and ITA_APL are excluded — they're BE-internal contract sub-variants of FRA and ITA, not onboarding country codes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…emaVersions resolvePinnedVersion now reads contract_details versions from ONBOARDING_JSON_SCHEMA_VERSION_BY_COUNTRY instead of a hand-maintained duplicate list, so the example app's schema versions stay the single source of truth. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…omise
checkSchemaBuildsAndValidates called form.handleValidation({}) without
awaiting it, so a throw during validation wasn't recorded as a failure
and could surface later as an unhandled rejection instead -- silently
marking a broken schema as a pass.
Flagged by Cursor Bugbot on PR #1382.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…htly report - pull_request trigger, path-filtered to schema-canary-relevant files, runs the pinned check only -- validates a version bump actually builds before merge, without adding sandbox load to unrelated PRs. - nightly/workflow_dispatch run keeps both checks, and now writes scripts/reports/schema-canary.json (via --write), committed through an auto-PR (peter-evans/create-pull-request), same pattern as sync-country-contract-versions.yml. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
client_credentials tokens aren't scoped to a company, but
GET /v1/countries/{country}/contract_details requires a company-scoped
token per its own generated type docs (src/client/types.gen.ts). Every
live request was failing 404 "Company not found" -- the client-credentials
grant this script used was never actually exercised against real
credentials before now.
Switch to the refresh_token grant, matching how example/api/get_token.js
authenticates for real flows. Verified against the sandbox: full sweep
now passes 182/182 checks (was 0/182).
Needs a new VITE_REFRESH_TOKEN (local .env.sandbox already has one) and
a SCHEMA_CANARY_REFRESH_TOKEN repo secret for CI.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fakeValueFor() hardcoded [true] for checkbox fields, mimicking a jsonType-array shape that doesn't match how the real form submits const-based checkboxes (e.g. FRA's ack_non_eligible_job_titles, a jsonType string with const: "acknowledged"). parseFormValuesToAPI swaps the RHF boolean for field.const at submit time; mirror that here instead of sending the raw fill value, or the API 422s with a type mismatch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fakeValueFor() hardcoded [true] for checkbox fields, mimicking a jsonType-array shape that doesn't match how the real form submits const-based checkboxes (e.g. FRA's ack_non_eligible_job_titles, a jsonType string with const: "acknowledged"). parseFormValuesToAPI swaps the RHF boolean for field.const at submit time; mirror that here instead of sending the raw fill value, or the API 422s with a type mismatch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…h employment_id
contract_details isn't purely a function of (country, version): the
gateway serves employment-scoped content too (e.g. a feature-flagged
"extreme salary acknowledgement" field using a money_format json-logic
operator). Every check so far -- pinned, latest, even raw fetches with
every query param variant -- only ever saw the generic no-employment-id
variant, so this whole class of regression was invisible to the canary.
- scripts/schema-canary/seed-employment.ts: creates a real employment
per country directly against the sandbox gateway (basic_information
-> contract-eligibility -> engagement_agreement_details, mirroring
seed-onboarding's flow), then schema-canary.ts fetches contract_details
with that employment_id for both pinned and latest. Verified end-to-end:
fails on the actually-shipped-before kit version, passes on current.
- Seeding failures (schema quirks the generic filler can't satisfy) mark
that country's checks as skip with the reason, rather than failing the
whole run.
- scripts/fill-schema.ts: the field-filling logic shared with
seed-onboarding, now including a holiday-aware provisional_start_date
(GET /v1/countries/{country}/holidays/{year}) -- a fixed date offset
alone isn't enough across ~90 countries checked nightly, some will
always land on a local holiday by chance. Also fixes job_title being
rejected by country-specific eligibility screening (lorem-ipsum and
even faker.person.jobTitle() got rejected; "Software Engineer" clears
it broadly).
- scripts/seed-onboarding.ts: converted from .mjs to .ts (via tsx) so it
can share scripts/fill-schema.ts directly instead of duplicating it --
also picks up the same job-title and holiday fixes.
- Live-verified: full 91-country sweep, 182/182 checks pass, 0 skips.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
create-pull-request had no if: condition, so GitHub Actions skipped it whenever the schema-canary step exited non-zero (a pinned failure) -- exactly the run where the report snapshot is most useful. The report is already written to disk before the exit code is set, so this is just about not skipping the commit step on that outcome. Flagged by Cursor Bugbot on PR #1382. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…details The engagement_agreement_details fetch used a bare catch, treating any error -- auth failure, 5xx, timeout -- the same as "this country has no such form" and letting seeding proceed without it. seed-onboarding.ts already gets this right (only swallows a 404); mirror that here. In practice every country returns 404 for this form except DEU, so this was latent rather than currently firing, but a real failure should skip that country's checks with a clear reason, not silently proceed against an incomplete employment. Flagged by Cursor Bugbot on PR #1382. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… strategy (#1459) * refactor(common): move useHeadlessForm's build and parse steps into plain functions buildHeadlessForm and parseValuesForValidation hold the per-strategy build and validation parsing with no React state, so code outside a component can run the exact path the hook runs. The hook's behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(schema-canary): check each country with its real useHeadlessForm strategy The canary built every schema with createHeadlessForm(schema, {}) and validated the raw values, which is only roughly the rebuild path. FRA, ITA, DEU and ESP run buildOnce in the Onboarding flow, which skips money conversion and parses values before validating, so a schema could break them and still pass. Each country now goes through buildHeadlessForm and parseValuesForValidation with the strategy the flow picks for it, and the report's engine column becomes strategy. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(lint): exempt headlessForm.ts and keep its builders behind useHeadlessForm headlessForm.ts now holds the createHeadlessForm call, so it joins the exempt files. buildHeadlessForm and parseValuesForValidation get the same restriction as createHeadlessForm, so flows still have to go through the hook; only the hook, tests and scripts/ can import them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor(onboarding): one source for the contract details strategy The canary guessed each country's strategy from usesJsfV1ContractDetails while the two contract details hooks hard-coded their own, so moving the legacy step to buildOnce would have left the canary checking rebuild. CONTRACT_DETAILS_STRATEGY now drives both hooks, and getContractDetailsStrategy reads from it for the canary. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ci(schema-canary): run the PR canary on dependency and form-pipeline changes A json-schema-form or form-kit bump only touches package.json and package-lock.json, so the PR canary never ran on it. Also trigger on the SDK files the canary actually exercises: createHeadlessForm, headlessForm, parseJSFToValidate and the Onboarding strategy selection. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts: # src/common/useHeadlessForm.ts
* feat(scripts): fill schemas until they pass validation
fillSchema only filled required fields by input type, so contract_details
values broke const, min/max, minLength and nested fieldsets: 0 of 92
pinned schemas validated. It now fills every visible field within the
field's own constraints (const, ranges, lengths, patterns via randexp,
date bounds, fieldsets), then regenerates only the fields that fail
handleValidation until there are no errors, the same approach Dragon's
smoke tests use. 91 of 92 pinned contract_details schemas now validate;
the remaining one (DEU without an employment) is unsatisfiable as served.
It also returns the remaining errors so callers can tell a filler gap
from a backend rejection, and no longer lists a required file field once
per pass.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(schema-canary): submit filled contract details per country
Adds a "submit" check: fill the pinned contract_details schema and send
the same PATCH /v1/employments/{id} the Onboarding flow sends, recording
the gateway's error body on rejection. It warns only for now and is
skipped when the pinned schema fails to build.
Countries now run six at a time and reuse the pinned schema for submit,
since the run time is sandbox latency, not filling. The nightly timeout
goes from 15 to 30 minutes for the extra PATCH per country.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(schema-canary): keep the nightly submit results stable between runs
Random submit values made a country like JAM pass or fail depending on
the run, and the error text carried that night's employment id, so the
committed report would change without any real change. Submit fills now
use a per-country faker seed (randexp draws from faker too) and the
error leaves the id out. Seeding stays random: faker is reseeded after a
seeded fill so reused emails can't collide.
Also reject a --concurrency that isn't a positive integer instead of
running zero checks and passing.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* ci(schema-canary): fail on submit rejections and run submit on PRs
A warning nobody reads isn't feedback: submit failures now fail the job
like pinned build failures, and the PR canary runs pinned and submit
(about two minutes with the country pool). No skip list for the known
backend rejections (CHN, ROU, JAM), so they stay red until fixed.
The PR job keeps its name so branch protection still matches it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(schema-canary): submit with the same seed values as seed-onboarding
The submit check filled CHN, JAM and ROU freely, so it kept hitting the
known backend bugs (Beijing province 500, undeclared ROU currency 500,
JAM hours range 422) instead of the payloads a real onboarding would
send. Move CONTRACT_DETAILS_SEEDS into a shared module and pass it to
the submit fill so both use the same fixed values.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…alues back (#1477) * feat(schema-canary): submit through the SDK form and read the saved values back The submit check sent the filler's raw values, so it skipped the SDK code a partner's submit goes through: the per-country useHeadlessForm strategy, money conversion and parseFormValues. It also only checked the PATCH status, not what was saved. submit now builds the pinned schema with the country's strategy, loads the filled values the way the Onboarding flow loads saved ones (cents to units), validates through the hook's path and parses them before the PATCH. It then GETs the employment and fails when a sent value was saved differently or not at all. overtime_eligible is excluded: Tiger moves it to the compensation record and the employment GET doesn't return it. withFieldsetObjects moves from useHeadlessForm into headlessForm so the canary validates exactly like the hook. FRA gets a fixed has_wage_portage_higher_degree: its seed answered no to both wage portage questions, so it submitted the ineligible branch with almost no fields. The SDK form caught that. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor(schema-canary): drop the inline comment on fields the employment GET omits Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…SDK form Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lures in the report PR A red nightly could go unnoticed. When the script crashed before writing the report (expired refresh token, sandbox down) no report PR was opened, a failure that was already in the report produced nothing new, and the report PR's title looked the same whether every country passed or not. The nightly now writes the failing checks to a file and, when the canary step fails, opens a "Schema canary is failing" issue labelled schema-canary with that table. Later failing runs edit it and comment only when the failures change, and the next green run closes it. The report PR title says how many checks are failing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… passed The close step used success(), so a failing later step such as create-pull-request skipped it and left the issue open after a green canary. Gate it on the canary step's own outcome, like the open step. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The read-back check skipped overtime_eligible everywhere, which hid two different cases behind one rule. ESP forces it (const equal to default), so the employment leaving it out loses nothing. GBR lets the user choose yes or no, and Tiger drops that answer: a real bug, being fixed. Forced fields are now left out of the comparison using the SDK's own checkFieldHasForcedValue, so any country's forced value is covered. GBR's overtime_eligible moves to a country-scoped KNOWN_UNSAVED_FIELDS entry that fails the check once the value is saved again, so the entry gets removed when Tiger's fix lands instead of silently outliving it. Verified live against sandbox: ESP and GBR submit pass, and both fail with the old blanket ignore removed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… save A forced value that comes back different is still a real mismatch. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts: # src/flows/Onboarding/tests/utils.test.ts # src/flows/Onboarding/utils.ts
…change The submit check reads CONTRACT_DETAILS_SEEDS, so a seed-only change would otherwise merge untested and fail overnight. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…1479) * feat(e2e): re-submit each country's contract details in the browser The schema canary proves the schema, the SDK form logic and the backend agree, but not that the rendered Onboarding flow does. For each country whose submit check passed, the browser stage now opens the employment in the example app, continues through basic information, renders contract details from the saved values, and re-submits it. It fails when the PATCH is rejected, when Continue is blocked, or when the body differs from what the canary sent. - schema-canary --keep-submitted=<file> keeps those employments and writes country, employment, company and the sent payload to <file>; schema-canary:archive archives them afterwards. - --country takes a comma-separated list. - ack_non_eligible_job_titles is not persisted, so the spec ticks it again on resume. KNOWN_UNSAVED_FIELDS entries (GBR overtime_eligible) must come back empty and are answered again from the sent value. - On PRs, schema-canary:affected runs only the countries whose pinned version, seed or known-unsaved entry changed, and every country when anything else the canary reads changed. Nightly runs every country. - example/src/flows/Onboarding/** now triggers the PR canary: its features and jsfModify change how contract details render. Verified locally: DEU, ESP, FRA and GBR pass in the browser; all 92 countries seed, submit and archive with --keep-submitted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(e2e): keep the browser canary under the sandbox rate limit The first CI run was blocked by the sandbox CloudFront firewall (403 "too much traffic"), which failed the proxy, left Select Country empty and stopped archiving, leaking employments. - The example app's proxy fetched a new OAuth token for every proxied request. It now caches each token until a minute before it expires. - archiveEmployment retries four times with backoff, so a temporary block can't leak employments. - The browser stage runs 3 workers on CI instead of 4. The browser stage also caught the canary drifting from the real form: on rebuild, a default that a condition turns on (ECU forces work_hours_per_week to 40 for full time) was only read from the first build, so the canary never sent it while the form did. The canary now re-reads defaults from the rebuilt fields until they stop changing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(e2e): write kept employments as they are kept The kept file was only written at the end of the run, so a timeout or cancel during the canary left kept employments off disk and the always() archive step had nothing to clean up. It's now rewritten after every kept country. Also run the PR canary, and every country, when the affected-countries script changes, and pin upload-artifact's comment to its exact tag. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ci(e2e): print each country as it finishes and cancel superseded PR canary runs The html and github reporters print nothing until the end, so the browser stage looked stuck. A newer push now cancels the older PR run instead of running both against sandbox at once; the always() archive step still cleans up the cancelled run. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ci(e2e): cache Playwright browsers in the browser canary like the e2e job Reuses ci-main's key (runner OS + Playwright version) so the canary shares the cached Chromium, and retries the system-dependency and browser installs instead of failing the run on one apt or download hiccup. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 49def7a. Configure here.
| }) | ||
| ).filter(Boolean); | ||
|
|
||
| process.exitCode = failures.length > 0 ? 1 : 0; |
There was a problem hiding this comment.
Archive failure fails a green canary
Medium Severity
schema-canary:archive sets a failing exit code when any DELETE fails, and the workflow runs that step with if: always() and no continue-on-error. A transient sandbox cleanup error therefore fails the job after pinned and browser checks already passed, while the failure-issue step still treats the canary as green and can close the tracking issue.
Additional Locations (2)
Triggered by project rule: Code Review Guidelines
Reviewed by Cursor Bugbot for commit 49def7a. Configure here.


Summary
Every night, and on PRs that touch form code, a job creates a real sandbox employment for each EOR country. It then checks that the country's live contract details form still builds and validates, and that filled-in values are accepted when submitted. If a country breaks, CI goes red before a partner hits it.
Why
What changed
Toggle details
What the canary checks per country (
npm run schema-canary,scripts/schema-canary.ts)For every country with
eor_onboarding: truein/v1/countries:employment_idthe way the flow does it. The employment is archived afterwards.contract_detailsat the version the example app pins (example/src/flows/Onboarding/jsonSchemaVersions.ts, falling back toDEFAULT_VERSION). Builds it with the sameuseHeadlessFormstrategy Onboarding uses for that country (buildOncefor jsf v1 countries,rebuildotherwise), then validates empty values.json_schema_version=latest. Only a warning, because it shows what is coming before we pin it.scripts/fill-schema.ts, plus the per-country seedsseed-onboardingalready uses) and sends the samePATCH /v1/employments/{id}the flow sends.skip-list.tsResults are printed as a markdown table and added to
$GITHUB_STEP_SUMMARY.--writealso saves them toscripts/reports/schema-canary.json.CI (
.github/workflows/schema-canary.yml)--write. Opens or updates an auto-PR (chore/schema-canary-report-auto) when the report changes. The report has no timestamp, so a quiet night opens no PR.--checks=pinned,submitwhen the files the canary depends on change (pinned versions, form build code, the canary itself, deps).SCHEMA_CANARY_CLIENT_ID/_CLIENT_SECRET/_REFRESH_TOKENrepo secrets (company-scoped refresh token). Locally it readsVITE_*from.env.sandbox.Changes to
src/(no public API changes)buildHeadlessForm/parseValuesForValidationmoved out ofuseHeadlessFormintosrc/common/headlessForm.ts. This lets the canary run the exact build + validate path outside React.useHeadlessFormnow calls them, with no change in behaviour.useHeadlessFormandscripts/, so they don't become a second way to build forms.CONTRACT_DETAILS_STRATEGY/getContractDetailsStrategy(country). The flow and the canary now pick the strategy from the same function.Other
CONTRACT_DETAILS_SEEDSmoved toscripts/contract-details-seeds.ts, shared byseed-onboardingand the canary.vitest.config.tsalso picks upscripts/**/*.test.tsx.Look closely at: the
useHeadlessFormrefactor (shared by every migrated form), and the fact that the canary writes real sandbox employments (archived after each country).Screenshots
N/A
Related Resources
COUNTRY_CONTRACT_VERSIONS(chore(JsonSchemaComparison): automate the weekly COUNTRY_CONTRACT_VERSIONS sync #1344), Slack alerting beyond a red run.Testing
Tested against the
example/app in a browser: N/A, this is a script and CI job.Feature flag: N/A
Unit tests:
scripts/schema-canary/lib.test.ts,submit-contract-details.test.ts, andgetContractDetailsStrategyinsrc/flows/Onboarding/tests/utils.test.ts.Live: the PR canary job runs pinned + submit against sandbox on this branch.
Locally:
npm run schema-canary -- --country=FRA(add--checks=pinned,submitor--writeas needed). Needs.env.sandbox.🤖 Generated with Claude Code
Note
Medium Risk
Touches shared headless form logic used by Onboarding and runs CI jobs that authenticate to sandbox and create/archive real employments, though behavior is intended to mirror existing flows.
Overview
Adds a contract details schema canary that exercises live sandbox
contract_detailsschemas per EOR country: seed employments, run pinned / latest (warning-only) / submit checks, and optionally keep successful submissions for a follow-up browser pass.CI and tooling: New
schema-canary.ymlruns nightly (full checks + committedscripts/reports/schema-canary.jsonvia auto-PR) and on path-filtered PRs (scoped countries viaschema-canary:affected). After--keep-submitted, a composite schema-canary-browser action starts the example app and Playwright canary tests assert the UI re-submits the samecontract_detailsPATCH payload. Employments are archived withschema-canary:archive. Nightly failures open/update aschema-canaryissue.Library alignment:
buildHeadlessForm,parseValuesForValidation, and related helpers move tosrc/common/headlessForm.tsso the canary anduseHeadlessFormshare Onboarding’s build/validate path; oxlint restricts direct imports. Onboarding usesgetContractDetailsStrategy/CONTRACT_DETAILS_STRATEGYconsistently with the canary. Per-country seeds live inscripts/contract-details-seeds.ts.Example app: OAuth token helpers in
get_token.jsare cached until expiry; main Playwright config ignorese2e/canary/**(separateplaywright.canary.config.ts).Reviewed by Cursor Bugbot for commit 49def7a. Bugbot is set up for automated code reviews on this repo. Configure here.