Skip to content

feat(e2e): add contract details schema canary script - #1382

Closed
gabrielseco wants to merge 60 commits into
mainfrom
feat/schema-canary
Closed

gabrielseco wants to merge 60 commits into
mainfrom
feat/schema-canary

Conversation

@gabrielseco

@gabrielseco gabrielseco commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

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

  • We can't count on every schema change being versioned. So we check the schemas partners actually load, with this repo's form code.
  • We check if contract_details can be submitted or not

What changed

Toggle details

What the canary checks per country (npm run schema-canary, scripts/schema-canary.ts)

For every country with eor_onboarding: true in /v1/countries:

  1. seed: creates a sandbox employment, so the schema is fetched with a real employment_id the way the flow does it. The employment is archived afterwards.
  2. pinned: fetches contract_details at the version the example app pins (example/src/flows/Onboarding/jsonSchemaVersions.ts, falling back to DEFAULT_VERSION). Builds it with the same useHeadlessForm strategy Onboarding uses for that country (buildOnce for jsf v1 countries, rebuild otherwise), then validates empty values.
  3. latest: same as pinned, at json_schema_version=latest. Only a warning, because it shows what is coming before we pin it.
  4. submit: fills the pinned schema with valid fake values (scripts/fill-schema.ts, plus the per-country seeds seed-onboarding already uses) and sends the same PATCH /v1/employments/{id} the flow sends.
Outcome Fails the job?
pinned / submit failure yes
seed error yes
latest failure no (warning)
entry in skip-list.ts no (currently empty)

Results are printed as a markdown table and added to $GITHUB_STEP_SUMMARY. --write also saves them to scripts/reports/schema-canary.json.

CI (.github/workflows/schema-canary.yml)

  • Nightly (04:00 UTC) + manual: runs every check with --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.
  • PR: runs --checks=pinned,submit when the files the canary depends on change (pinned versions, form build code, the canary itself, deps).
  • Uses the SCHEMA_CANARY_CLIENT_ID / _CLIENT_SECRET / _REFRESH_TOKEN repo secrets (company-scoped refresh token). Locally it reads VITE_* from .env.sandbox.

Changes to src/ (no public API changes)

  • buildHeadlessForm / parseValuesForValidation moved out of useHeadlessForm into src/common/headlessForm.ts. This lets the canary run the exact build + validate path outside React. useHeadlessForm now calls them, with no change in behaviour.
  • New oxlint rule: these two can only be imported by useHeadlessForm and scripts/, so they don't become a second way to build forms.
  • Onboarding gets CONTRACT_DETAILS_STRATEGY / getContractDetailsStrategy(country). The flow and the canary now pick the strategy from the same function.

Other

  • CONTRACT_DETAILS_SEEDS moved to scripts/contract-details-seeds.ts, shared by seed-onboarding and the canary.
  • vitest.config.ts also picks up scripts/**/*.test.tsx.

Look closely at: the useHeadlessForm refactor (shared by every migrated form), and the fact that the canary writes real sandbox employments (archived after each country).

Screenshots

N/A

Related Resources

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, and getContractDetailsStrategy in src/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,submit or --write as 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_details schemas 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.yml runs nightly (full checks + committed scripts/reports/schema-canary.json via auto-PR) and on path-filtered PRs (scoped countries via schema-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 same contract_details PATCH payload. Employments are archived with schema-canary:archive. Nightly failures open/update a schema-canary issue.

Library alignment: buildHeadlessForm, parseValuesForValidation, and related helpers move to src/common/headlessForm.ts so the canary and useHeadlessForm share Onboarding’s build/validate path; oxlint restricts direct imports. Onboarding uses getContractDetailsStrategy / CONTRACT_DETAILS_STRATEGY consistently with the canary. Per-country seeds live in scripts/contract-details-seeds.ts.

Example app: OAuth token helpers in get_token.js are cached until expiry; main Playwright config ignores e2e/canary/** (separate playwright.canary.config.ts).

Reviewed by Cursor Bugbot for commit 49def7a. Bugbot is set up for automated code reviews on this repo. Configure here.

gabrielseco and others added 9 commits September 23, 2026 09:38
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>
@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

Metric Current Previous Change Status
Total (gzip) 228.91 kB 228.77 kB +143 B (+0.1%) 🔴
Total (raw) 637.74 kB 637.47 kB +272 B (+0.0%) 🔴
CSS (gzip) 21.94 kB 21.94 kB 0 B (0%) 🟢
CSS (raw) 114.43 kB 114.43 kB 0 B (0%) 🟢

Size Limits

  • ✅ Total gzipped: 228.91 kB / 350 kB (65.4%)
  • ✅ Total raw: 637.74 kB / 850 kB (75.0%)
  • ✅ CSS gzipped: 21.94 kB / 25 kB (87.8%)

Largest Files (Top 5)

  1. index.esm-wIkXrqU6.js - 11.4 kB (0 B (0%))
  2. styles.css - 10.97 kB (0 B (0%))
  3. index.css - 10.97 kB (0 B (0%))
  4. internals-Bfee4wlS.js - 6.12 kB (0 B (0%))
  5. hooks-B1CnEL_O.js - 5.84 kB (new)
View All Files (291 total)
File Size (gzip) Change
index.esm-wIkXrqU6.js 11.4 kB 0 B (0%)
styles.css 10.97 kB 0 B (0%)
index.css 10.97 kB 0 B (0%)
internals-Bfee4wlS.js 6.12 kB 0 B (0%)
hooks-B1CnEL_O.js 5.84 kB new
sdk.gen-FtLNfTcH.js 5.54 kB 0 B (0%)
index.js 5.51 kB 0 B (0%)
flows/Onboarding/hooks.js 4.52 kB 0 B (0%)
utils-CUgvJHl4.js 4.07 kB 0 B (0%)
FieldSetField-CDERaaih.js 4.03 kB 0 B (0%)

✅ Bundle size check passed

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Deploy preview for adp-cost-calculator ready!

Project:adp-cost-calculator
Status: ✅  Deploy successful!
Preview URL:https://adp-cost-calculator-o2x43t0le-remotecom.vercel.app
Latest Commit:49def7a

Deployed with vercel-action

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Deploy preview for remote-flows ready!

Project:remote-flows
Status: ✅  Deploy successful!
Preview URL:https://remote-flows-9l06noy3v-remotecom.vercel.app
Latest Commit:49def7a

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>
@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

📊 Coverage Report

✅ Coverage increased! 🎉

Metric Current Previous Change Status
Lines 89.33% 89.31% +0.01% 🟢
Statements 88.88% 88.86% +0.01% 🟢
Functions 88.09% 88.07% +0.02% 🟢
Branches 80.38% 80.36% +0.02% 🟢

Detailed Breakdown

Lines Coverage
  • Covered: 5256 / 5884
  • Coverage: 89.33%
  • Change: +0.01% (7 lines)
Statements Coverage
  • Covered: 5354 / 6024
  • Coverage: 88.88%
  • Change: +0.01% (7 statements)
Functions Coverage
  • Covered: 1405 / 1595
  • Coverage: 88.09%
  • Change: +0.02% (3 functions)
Branches Coverage
  • Covered: 3253 / 4047
  • Coverage: 80.38%
  • Change: +0.02% (4 branches)

✅ Coverage check passed

gabrielseco and others added 5 commits September 23, 2026 13:36
…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>

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread scripts/schema-canary/lib.ts Resolved
gabrielseco and others added 2 commits September 23, 2026 15:01
…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>

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread .github/workflows/schema-canary.yml Resolved
gabrielseco and others added 4 commits September 23, 2026 15:11
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>

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

gabrielseco and others added 2 commits September 23, 2026 16:53
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>
gabrielseco and others added 2 commits October 7, 2026 11:08
… 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
gabrielseco and others added 3 commits October 8, 2026 13:12
* 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>
gabrielseco and others added 3 commits October 8, 2026 14:00
…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>

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread .github/workflows/schema-canary.yml Resolved
… 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>
@gabrielseco gabrielseco self-assigned this Oct 8, 2026
gabrielseco and others added 3 commits October 8, 2026 15:22
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

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread .github/workflows/schema-canary.yml Resolved
…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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Triggered by project rule: Code Review Guidelines

Reviewed by Cursor Bugbot for commit 49def7a. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant