From e3cecf9866696eaddc44768451b24a221f0d521c Mon Sep 17 00:00:00 2001
From: abrichr
Date: Sun, 26 Jul 2026 15:28:52 -0400
Subject: [PATCH] feat(site): comparison pages, partners program, remote-mode
diagram, intake polish (Sections 18+21)
- /compare/ pages for UiPath, Power Automate, computer-use agents,
record-and-replay tools, browser-agent platforms, and hand-rolled
scripts, driven by data/comparisons.js under enforced honesty rules:
credit real competitor strengths, differentiate only on independent
effect verification, explicit outcomes, the external zero-install
remote lane, customer-controlled data, deterministic model-free
healthy runs, the MIT runtime, and qualification evidence; never on
commoditized recording/visual-targeting/Citrix/self-healing.
- /partners page with four tracks (vertical software/OEM, RCM+BPO,
integration/services, MSP/deployment), each stating model and
qualification/support/packs boundaries, plus a dedicated Netlify
partner-inquiry intake with a track selector registered in
public/form.html. Status-honest: application-based, no self-serve
portal.
- Remote execution modes figure on /how-it-works: in-session vs
external black-box, labeled per current qualification status
(deterministic stand-in qualified; real ICA/HDX per customer).
- Nav/footer: Partners & OEM in Solutions dropdown and footer Company
column; Contribute workflows in footer Connect column.
- Qualification intake: required privacy-consent checkbox linking the
Privacy Notice, captured leadSegment field, method=POST fallback, and
node + Cypress tests asserting no lead field ever appears in a URL.
Co-Authored-By: Claude Opus 4.8
Claude-Session: https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM
---
components/Footer.js | 6 +
components/NavHeader.js | 1 +
components/PartnerInquiryForm.js | 251 ++++++++++++++++++++++++
components/RemoteModesFigure.js | 184 +++++++++++++++++
components/WorkflowQualificationForm.js | 36 +++-
cypress/e2e/lead-privacy.cy.js | 99 ++++++++++
data/comparisons.js | 199 +++++++++++++++++++
pages/compare.js | 36 ++++
pages/compare/[slug].js | 235 ++++++++++++++++++++++
pages/how-it-works.js | 5 +
pages/partners.js | 202 +++++++++++++++++++
public/form.html | 15 ++
public/llms.txt | 7 +
public/sitemap.xml | 42 ++++
tests/comparisonPages.test.js | 150 ++++++++++++++
tests/leadPrivacy.test.js | 110 +++++++++++
tests/partnersProgram.test.js | 98 +++++++++
17 files changed, 1674 insertions(+), 2 deletions(-)
create mode 100644 components/PartnerInquiryForm.js
create mode 100644 components/RemoteModesFigure.js
create mode 100644 cypress/e2e/lead-privacy.cy.js
create mode 100644 data/comparisons.js
create mode 100644 pages/compare/[slug].js
create mode 100644 pages/partners.js
create mode 100644 tests/comparisonPages.test.js
create mode 100644 tests/leadPrivacy.test.js
create mode 100644 tests/partnersProgram.test.js
diff --git a/components/Footer.js b/components/Footer.js
index dffa64c5..01f39207 100644
--- a/components/Footer.js
+++ b/components/Footer.js
@@ -171,6 +171,7 @@ const CONNECT_COLUMN = [
BLOG_LINK,
byLabel('Discord'),
{ label: 'GitHub', href: OPENADAPT_REPOSITORY_URL },
+ { label: 'Contribute workflows', href: '/contribute' },
]
export default function Footer({
@@ -360,6 +361,11 @@ export default function Footer({
About
+
+
+ Partners & OEM
+
+
Hosted dashboard
diff --git a/components/NavHeader.js b/components/NavHeader.js
index bcdf7524..6d6ad6ed 100644
--- a/components/NavHeader.js
+++ b/components/NavHeader.js
@@ -26,6 +26,7 @@ const SOLUTIONS_LINKS = [
{ label: 'Healthcare', href: '/solutions/healthcare' },
{ label: 'Lending', href: '/solutions/lending' },
{ label: 'Insurance', href: '/solutions/insurance' },
+ { label: 'Partners & OEM', href: '/partners' },
]
const PRODUCT_LINKS = [
diff --git a/components/PartnerInquiryForm.js b/components/PartnerInquiryForm.js
new file mode 100644
index 00000000..ea92b2d3
--- /dev/null
+++ b/components/PartnerInquiryForm.js
@@ -0,0 +1,251 @@
+import { useState } from 'react'
+import Link from 'next/link'
+
+import { trackEmailCapture } from 'utils/conversion'
+
+// Dedicated intake for the partner program.
+//
+// Posts to the durable Netlify Forms lead path (`/form.html`) under its own
+// form name, `partner-inquiry`, so partner leads land in their own Netlify
+// submissions bucket instead of the generic `contact` queue. The hidden form
+// definition lives in public/form.html; Netlify's build-time bots read it
+// from there. The `track` selector mirrors the partner tracks on /partners.
+// Lead data always travels in the POST body, never in a URL.
+
+export const PARTNER_TRACKS = [
+ { value: 'vertical_oem', label: 'Vertical software / OEM' },
+ { value: 'rcm_bpo', label: 'RCM or BPO operator' },
+ { value: 'integration_services', label: 'Integration / services' },
+ { value: 'msp_deployment', label: 'MSP / deployment' },
+]
+
+const INITIAL_FORM = {
+ name: '',
+ email: '',
+ company: '',
+ role: '',
+ track: '',
+ systems: '',
+ message: '',
+ botField: '',
+}
+
+const fieldClass =
+ 'rounded-lg border border-ink/30 bg-panel px-3 py-2 text-ink placeholder-ink-3/60 focus:border-accent focus:outline-none'
+
+export default function PartnerInquiryForm() {
+ const [form, setForm] = useState(INITIAL_FORM)
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const [isSubmitted, setIsSubmitted] = useState(false)
+ const [error, setError] = useState('')
+
+ const handleChange = (event) => {
+ const { name, value } = event.target
+ setForm((prev) => ({ ...prev, [name]: value }))
+ }
+
+ const handleSubmit = async (event) => {
+ event.preventDefault()
+ setError('')
+ setIsSubmitting(true)
+
+ try {
+ const formData = new URLSearchParams()
+ formData.set('form-name', 'partner-inquiry')
+ formData.set('name', form.name)
+ formData.set('email', form.email)
+ formData.set('company', form.company)
+ formData.set('role', form.role)
+ formData.set('track', form.track)
+ formData.set('systems', form.systems)
+ formData.set('message', form.message)
+ formData.set('bot-field', form.botField)
+
+ const response = await fetch('/form.html', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: formData.toString(),
+ })
+
+ if (!response.ok) {
+ throw new Error(
+ `Form submission failed with status ${response.status}`
+ )
+ }
+
+ // Qualified-lead conversion: partner interest captured. Carries
+ // first-touch utm_* attribution; never any form contents.
+ trackEmailCapture({ location: 'partner_inquiry_form' })
+ setIsSubmitted(true)
+ } catch (submitError) {
+ console.error(submitError)
+ setError(
+ 'Submission failed. Please email hello@openadapt.ai directly.'
+ )
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ return (
+
+ {!isSubmitted ? (
+
+ ) : (
+
+
+ Thanks. Your partner application is in review. We
+ evaluate applications individually and will reply by
+ email.
+
+
+ )}
+
+ )
+}
diff --git a/components/RemoteModesFigure.js b/components/RemoteModesFigure.js
new file mode 100644
index 00000000..0602aa4d
--- /dev/null
+++ b/components/RemoteModesFigure.js
@@ -0,0 +1,184 @@
+import Link from 'next/link'
+
+// The two remote execution modes, side by side, as a styled HTML/CSS figure.
+//
+// Honesty contract (kept in sync with public/status.json): the external lane
+// is qualified today against a deterministic no-DOM stand-in for the Citrix
+// Workspace window contract plus a real FreeRDP round trip for RDP; a real
+// ICA/HDX environment is qualified per customer before consequential use.
+// Neither panel claims universal production coverage.
+
+function DownArrow({ label }) {
+ return (
+
+
+
+
+ {label && (
+
+ {label}
+
+ )}
+
+ )
+}
+
+function Box({ title, detail, tone = 'default' }) {
+ const toneClass =
+ tone === 'accent'
+ ? 'border-accent/60 bg-accent/10'
+ : tone === 'dashed'
+ ? 'border-dashed border-ink/40 bg-ground'
+ : 'border-hairline bg-panel'
+ return (
+
+
{title}
+ {detail && (
+
+ {detail}
+
+ )}
+
+ )
+}
+
+export default function RemoteModesFigure() {
+ return (
+
+
+
Remote execution modes
+
+ Two ways into managed Citrix, RDP, and VDI estates
+
+
+ Where policy permits software inside the managed session,
+ the runner executes in-session. Where it does not, the
+ external lane drives the local remote-desktop client
+ window from outside, so nothing is installed in the remote
+ environment. Both modes keep the same identity, policy,
+ verification, and halt contracts.
+
+
+
+
+
+
+ In-session
+
+
+ The runner is installed inside the managed
+ session, when your policy permits it.
+
+
+
+
+ Managed session (Citrix / RDP / VDI)
+
+
+
+
+
+
+
+
+
+ Requires software inside the session image, so
+ it fits estates where you control the image or
+ policy allows an installed agent.
+
+
+
+
+
+ External black-box
+
+
+ The runner drives the local Citrix or RDP
+ client window. Zero install inside the remote
+ session.
+
+
+
+
+ Your workstation
+
+
+
+
+
+
+
+
+
+
+
+ Fits locked-down estates where nothing may be
+ installed in the managed environment.
+
+
+
+
+ Status, stated plainly: the external lane is qualified
+ today against a deterministic no-DOM stand-in for the
+ Citrix Workspace window contract and a real FreeRDP
+ round trip for RDP. A real ICA/HDX environment is
+ qualified per customer before consequential use. Per
+ surface results are published in the{' '}
+
+ qualification evidence
+
+ , and machine-readable status in{' '}
+
+ status.json
+
+ .
+
+
+
+
+ )
+}
diff --git a/components/WorkflowQualificationForm.js b/components/WorkflowQualificationForm.js
index e77fe36d..88871d06 100644
--- a/components/WorkflowQualificationForm.js
+++ b/components/WorkflowQualificationForm.js
@@ -32,6 +32,8 @@ const INITIAL_FORM = {
timeline: '',
budget: '',
reusePotential: '',
+ leadSegment: '',
+ privacyConsent: '',
botField: '',
}
@@ -71,8 +73,11 @@ export default function WorkflowQualificationForm({ compact = false }) {
const handleChange = (event) => {
markStarted()
- const { name, value } = event.target
- setForm((current) => ({ ...current, [name]: value }))
+ const { name, value, type, checked } = event.target
+ // The consent checkbox stores 'yes' or '' so every field serializes
+ // uniformly into the POST body (never into a URL).
+ const next = type === 'checkbox' ? (checked ? 'yes' : '') : value
+ setForm((current) => ({ ...current, [name]: next }))
}
const handleSubmit = async (event) => {
@@ -152,6 +157,7 @@ export default function WorkflowQualificationForm({ compact = false }) {
name="workflow-qualification"
data-netlify="true"
netlify-honeypot="bot-field"
+ method="POST"
onSubmit={handleSubmit}
className="space-y-7"
>
@@ -210,6 +216,13 @@ export default function WorkflowQualificationForm({ compact = false }) {
Regulated or clinical data
Highly restricted
+
+ Enterprise operator automating our own workflows
+ Vertical software vendor / OEM
+ BPO, RCM, or services provider
+ Developer or technical evaluator
+ Community / open-source user
+
@@ -333,6 +346,25 @@ export default function WorkflowQualificationForm({ compact = false }) {
)}
+
+
+
+ I consent to OpenAdapt processing the details above to
+ review this workflow and respond, as described in the{' '}
+
+ Privacy Notice
+
+ . *
+
+
+
{
+ it('submits qualification leads in a POST body and never in a URL', () => {
+ cy.intercept('POST', '/form.html', {
+ statusCode: 200,
+ body: '',
+ }).as('leadSubmit')
+
+ cy.visit('/qualify')
+
+ cy.get('form[name="workflow-qualification"]').within(() => {
+ for (const [field, value] of Object.entries(LEAD)) {
+ cy.get(`[name="${field}"]`).type(value, { delay: 0 })
+ }
+ for (const [field, value] of Object.entries(SELECTS)) {
+ cy.get(`select[name="${field}"]`).select(value)
+ }
+ cy.get('input[name="privacyConsent"]').check()
+ cy.get('button[type="submit"]').click()
+ })
+
+ cy.wait('@leadSubmit').then(({ request }) => {
+ // The submission target carries no query string.
+ const requestUrl = new URL(request.url)
+ expect(requestUrl.pathname).to.equal('/form.html')
+ expect(requestUrl.search).to.equal('')
+
+ // The lead fields travel in the encoded body, including the new
+ // consent and segment captures.
+ const body = new URLSearchParams(request.body)
+ expect(body.get('form-name')).to.equal('workflow-qualification')
+ expect(body.get('email')).to.equal(LEAD.email)
+ expect(body.get('leadSegment')).to.equal('enterprise_operator')
+ expect(body.get('privacyConsent')).to.equal('yes')
+ expect(body.get('qualificationTier')).to.be.oneOf([
+ 'priority',
+ 'review',
+ 'community',
+ ])
+ })
+
+ // The form reached its submitted state without the browser URL
+ // picking up any lead value.
+ cy.contains('Submission received').should('be.visible')
+ cy.url().should((url) => {
+ expect(url.endsWith('/qualify')).to.equal(true)
+ for (const value of [
+ LEAD.email,
+ encodeURIComponent(LEAD.email),
+ LEAD.name,
+ encodeURIComponent(LEAD.name),
+ ]) {
+ expect(url).to.not.include(value)
+ }
+ })
+ })
+
+ it('requires the privacy consent checkbox before submitting', () => {
+ cy.visit('/qualify')
+ cy.get('input[name="privacyConsent"]')
+ .should('have.attr', 'required')
+ cy.get('input[name="privacyConsent"]').should('not.be.checked')
+ cy.get('form[name="workflow-qualification"]')
+ .contains('a', 'Privacy Notice')
+ .should('have.attr', 'href', '/privacy-policy')
+ })
+})
diff --git a/data/comparisons.js b/data/comparisons.js
new file mode 100644
index 00000000..c5882c51
--- /dev/null
+++ b/data/comparisons.js
@@ -0,0 +1,199 @@
+// Canonical content for the /compare/ alternative pages.
+//
+// Honesty rules (enforced by tests/comparisonPages.test.js):
+// - Credit each alternative for its real strengths in its own section.
+// - Differentiate OpenAdapt only on what is real today: independent
+// out-of-band business-effect verification, explicit transaction outcomes,
+// the external zero-install remote execution lane, customer-controlled
+// sensitive data, deterministic zero-model-call healthy runs, the open MIT
+// local runtime, and published qualification evidence.
+// - Never differentiate on recording demos, visual targeting, Citrix
+// awareness, or self-healing: those capabilities are widely available.
+// - Never state competitor pricing beyond well-known published figures, and
+// phrase those as "published pricing starts around ...".
+
+export const OPENADAPT_DIFFERENTIATORS = [
+ {
+ title: 'Independent business-effect verification',
+ body: 'A run is judged by an out-of-band check of the system of record, such as a read-only API call, a SQL query, or re-reading the persisted record, not by the acting session declaring itself successful.',
+ },
+ {
+ title: 'Explicit transaction outcomes',
+ body: 'Every consequential run ends verified or halted with a preserved run report. There is no silent third state where the workflow looked finished but the record never changed.',
+ },
+ {
+ title: 'Deterministic healthy runs',
+ body: 'A compiled workflow replays deterministically with zero model calls on healthy runs. Model spend is reserved for compilation and reviewable repair.',
+ },
+ {
+ title: 'External zero-install remote lane',
+ body: 'For managed Citrix, RDP, and VDI estates, OpenAdapt can drive the local client window from outside the session, so nothing is installed inside the remote environment. The lane is qualified today against a deterministic stand-in and a real FreeRDP round trip; a real ICA/HDX environment is qualified per customer before consequential use.',
+ },
+ {
+ title: 'Customer-controlled sensitive data',
+ body: 'Recordings, screenshots, and compiled bundles can stay inside your boundary. Local, self-hosted, and customer-controlled deployments are first-class, not an enterprise afterthought.',
+ },
+ {
+ title: 'Open MIT local runtime',
+ body: 'The compiler and governed runtime are MIT-licensed and inspectable. You can audit exactly what runs beside your systems of record.',
+ },
+ {
+ title: 'Published qualification evidence',
+ body: 'Each execution surface ships with bounded, published acceptance evidence, counted effects, refusals, and halts, instead of an unbounded compatibility claim.',
+ },
+]
+
+const QUALIFICATION_EVIDENCE_URL =
+ 'https://docs.openadapt.ai/get-started/what-works-today/'
+
+export const COMPARISONS = [
+ {
+ slug: 'uipath',
+ name: 'UiPath',
+ title: 'OpenAdapt vs UiPath',
+ metaDescription:
+ 'When UiPath is the right choice, when OpenAdapt is, and how independent effect verification, explicit outcomes, and an open MIT runtime differ from platform RPA.',
+ intro: 'UiPath is the most mature enterprise RPA platform. OpenAdapt is a governed workflow compiler with independent effect verification. They solve overlapping problems from different starting points, and for many teams the honest answer is one, the other, or both.',
+ theirStrengths: {
+ heading: 'Where UiPath is strong',
+ items: [
+ 'A mature, widely deployed enterprise platform with a long production track record and deep institutional knowledge in the market.',
+ 'Orchestrator provides scheduling, work queues, credential vaults, role-based access, and centralized audit across large robot fleets.',
+ 'Mature, long-standing support for automating Citrix and other virtual desktop environments, including dedicated remote-runtime integrations.',
+ 'A large marketplace, extensive training through UiPath Academy, and a broad certified-partner and developer ecosystem.',
+ 'Attended and unattended robots, a very large activity library, and adjacent products for document understanding and process mining.',
+ ],
+ },
+ chooseThem:
+ 'Choose UiPath when you are standardizing hundreds of automations on one enterprise platform, need mature fleet orchestration today, or already have UiPath licenses, developers, and a center of excellence.',
+ chooseUs:
+ 'Choose OpenAdapt when the workflows that matter are consequential transactions where you need the system of record independently confirmed after every run, when sensitive data must stay in your boundary on an inspectable MIT runtime, or when you want a zero-install lane into managed remote estates.',
+ honestNote:
+ 'Recording a demonstration, visual targeting, and repairing broken selectors are not differentiators in either direction: modern RPA platforms and OpenAdapt all do these. The difference is how a run is judged and what a failure is allowed to look like.',
+ },
+ {
+ slug: 'power-automate',
+ name: 'Microsoft Power Automate',
+ title: 'OpenAdapt vs Microsoft Power Automate',
+ metaDescription:
+ 'When Power Automate is the right choice, when OpenAdapt is, and how independent verification and customer-controlled execution differ from ecosystem RPA.',
+ intro: 'Power Automate is the default automation layer of the Microsoft ecosystem, and for Microsoft-centric work it is very hard to beat on integration and price. OpenAdapt is a governed workflow compiler focused on consequential GUI transactions with independent verification.',
+ theirStrengths: {
+ heading: 'Where Power Automate is strong',
+ items: [
+ 'Deep, native integration with Microsoft 365, Teams, Excel, SharePoint, Dataverse, and Azure identity.',
+ 'Hundreds of prebuilt connectors for cloud flows, so API-first automation often needs no custom code at all.',
+ 'Power Automate for desktop is included with Windows 11 for attended local flows, which makes experimentation essentially free.',
+ 'Aggressive published pricing for an enterprise suite: premium per-user plans start around $15 per user per month, and hosted RPA bots are in the roughly $215 per bot per month class.',
+ 'Centralized administration and governance through the Power Platform admin center for organizations already run on Microsoft tooling.',
+ ],
+ },
+ chooseThem:
+ 'Choose Power Automate when the work lives inside the Microsoft ecosystem, when a supported connector already reaches the system you need, or when low per-seat cost across many light automations matters more than transaction-level verification.',
+ chooseUs:
+ 'Choose OpenAdapt when the last mile is a non-Microsoft or legacy GUI, when a wrong write is expensive enough that you need the business effect independently verified out of band, when runs must end in an explicit verified-or-halted outcome, or when the runtime must be open, local, and customer-controlled.',
+ honestNote:
+ 'If a supported connector or API completes the workflow reliably, use it. OpenAdapt exists for the UI-only remainder where no practical API exists and correctness has to be proved, not assumed.',
+ },
+ {
+ slug: 'computer-use-agents',
+ name: 'computer-use agents',
+ title: 'OpenAdapt vs computer-use agents',
+ metaDescription:
+ 'When Claude or OpenAI computer use is the right choice, when OpenAdapt is, and why deterministic replay with independent verification fits repeated consequential work.',
+ intro: 'Computer-use agents from Anthropic and OpenAI point a frontier model at the screen and let it reason its way through a task. That flexibility is real and improving fast. OpenAdapt compiles a demonstration once and replays it deterministically, reserving models for compilation and repair.',
+ theirStrengths: {
+ heading: 'Where computer-use agents are strong',
+ items: [
+ 'Genuine flexibility on novel, one-off, or loosely specified tasks, with no per-workflow setup or authoring step.',
+ 'They generalize across unfamiliar interfaces and recover from situations no one anticipated in advance.',
+ 'Capability improves with every model generation, without any change to your workflow definitions.',
+ 'A plain-language instruction is the whole interface, which makes them accessible to anyone who can describe the task.',
+ ],
+ },
+ chooseThem:
+ 'Choose a computer-use agent for exploratory, novel, or constantly changing work, for research and triage, or for tasks you will run a handful of times and never again.',
+ chooseUs:
+ 'Choose OpenAdapt when the same consequential workflow repeats: healthy runs are deterministic with zero model calls and zero per-run token cost, every run ends verified or halted against an independent check of the system of record, and the runtime is MIT-licensed and can execute entirely inside your boundary.',
+ honestNote:
+ 'These approaches are complementary rather than rivals: agent providers themselves recommend human oversight for consequential actions, and OpenAdapt uses models too, at compile and repair time rather than on every healthy run. The question is whether each run should re-reason the task or replay a verified program.',
+ },
+ {
+ slug: 'record-and-replay',
+ name: 'record-and-replay tools',
+ title: 'OpenAdapt vs record-and-replay tools',
+ metaDescription:
+ 'When record-and-replay tools, including OpenAI Record & Replay, are the right choice, and what independent verification and explicit outcomes add for consequential work.',
+ intro: 'Recording a demonstration and replaying it is now a mainstream authoring model, from classic macro recorders to OpenAI shipping Record & Replay for repeated tasks. OpenAdapt shares the authoring model, so the real comparison is what happens after the recording, not the recording itself.',
+ theirStrengths: {
+ heading: 'Where record-and-replay tools are strong',
+ items: [
+ 'The fastest possible authoring: demonstrate the task once and it becomes repeatable, with no selectors or code.',
+ 'Accessible to non-developers, so the people who actually do the work can automate it themselves.',
+ 'Replay avoids re-reasoning the task from scratch on every run, which keeps repeat runs fast and cheap.',
+ "OpenAI's Record & Replay brings demonstration-based authoring to a mainstream audience, which validates the model for everyone building on it.",
+ ],
+ },
+ chooseThem:
+ 'Choose a lightweight record-and-replay tool for personal productivity, low-stakes repetitive tasks, and workflows where an occasional silent miss costs little.',
+ chooseUs:
+ 'Choose OpenAdapt when a replayed action writes to a system of record that matters: the compiled program is reviewable, healthy runs are deterministic with zero model calls, the business effect is verified out of band after the run, ambiguity halts instead of guessing, and the MIT runtime plus your data can stay entirely inside your boundary.',
+ honestNote:
+ 'Recording is the commodity; OpenAdapt does not claim to record better. The difference is governance: an independent verifier, an explicit verified-or-halted outcome for every run, and published qualification evidence per execution surface.',
+ },
+ {
+ slug: 'browser-agents',
+ name: 'browser-agent platforms',
+ title: 'OpenAdapt vs browser-agent platforms',
+ metaDescription:
+ 'When browser-agent platforms are the right choice, when OpenAdapt is, and how independent verification and customer-controlled execution differ for consequential web work.',
+ intro: 'Browser-agent platforms combine web automation frameworks, language models, and often managed cloud browsers into a fast way to automate web tasks. For prototyping and web-only work they are excellent. OpenAdapt targets repeated, consequential workflows across browser, desktop, and remote surfaces with verification at the center.',
+ theirStrengths: {
+ heading: 'Where browser-agent platforms are strong',
+ items: [
+ 'Very fast setup for web tasks: point an agent at a URL and useful behavior often emerges in minutes.',
+ 'Managed cloud-browser infrastructure removes the burden of running and scaling browsers yourself.',
+ 'Strong fit for scraping, research, monitoring, and other read-heavy web work where a retry is cheap.',
+ 'Active open-source and commercial ecosystems iterating quickly on web-agent capability.',
+ ],
+ },
+ chooseThem:
+ 'Choose a browser-agent platform for web-only, read-heavy, or exploratory automation, for prototypes, and for workloads where a failed or repeated attempt has little cost.',
+ chooseUs:
+ 'Choose OpenAdapt when the workflow also crosses desktop or remote surfaces, when the acting session must not be the judge of its own success, when every consequential run needs an explicit verified-or-halted outcome, or when sensitive data cannot transit a vendor cloud and must run on a customer-controlled MIT runtime.',
+ honestNote:
+ 'Most browser-agent stacks judge success in-band: the same session that acted decides whether it worked. OpenAdapt verifies the business effect out of band, against the system of record, after the run.',
+ },
+ {
+ slug: 'hand-rolled-scripts',
+ name: 'hand-rolled scripts',
+ title: 'OpenAdapt vs hand-rolled scripts',
+ metaDescription:
+ 'When Playwright, Selenium, or AutoHotkey scripts are the right choice, when OpenAdapt is, and what governance adds beyond a working script.',
+ intro: 'A script written with Playwright, Selenium, AutoHotkey, or PyAutoGUI is free, precise, and completely under your control. Plenty of production automation runs this way for good reasons. OpenAdapt is also open and local; what it adds is the governance around the script you would otherwise build yourself.',
+ theirStrengths: {
+ heading: 'Where hand-rolled scripts are strong',
+ items: [
+ 'Zero license cost and no vendor relationship: the whole stack is open source and yours.',
+ 'Exact control over every step, wait, and assertion, with the full power of a general-purpose language.',
+ 'Mature frameworks, huge communities, and years of accumulated answers for almost any situation.',
+ 'For a developer who owns the workflow, a small script is often the fastest and simplest correct answer.',
+ ],
+ },
+ chooseThem:
+ 'Keep a hand-rolled script when it already works, a developer owns and maintains it, and its failure modes are understood and affordable.',
+ chooseUs:
+ 'Choose OpenAdapt when scripts have become an unowned maintenance burden, when non-developers need to author workflows by demonstration, or when you need what scripts rarely include: independent out-of-band verification of the business effect, an explicit verified-or-halted outcome with a preserved run report, and halts on ambiguity instead of best-effort clicks. The runtime is MIT-licensed, so you trade none of the openness.',
+ honestNote:
+ 'OpenAdapt does not claim your script cannot work. It packages the verification, outcome discipline, and audit trail that consequential scripts eventually grow by hand, and it publishes qualification evidence per surface instead of assuming coverage.',
+ },
+]
+
+export const COMPARISON_LINKS = COMPARISONS.map(({ slug, name, title }) => ({
+ slug,
+ name,
+ title,
+ href: `/compare/${slug}`,
+}))
+
+export { QUALIFICATION_EVIDENCE_URL }
diff --git a/pages/compare.js b/pages/compare.js
index bada2413..0c8d6704 100644
--- a/pages/compare.js
+++ b/pages/compare.js
@@ -6,6 +6,7 @@ import { track, EVENTS } from 'utils/analytics'
import BenchmarkCharts from '@components/BenchmarkCharts'
import Faq, { faqItems } from '@components/Faq'
import benchmark from '../data/benchmark.json'
+import { COMPARISON_LINKS } from '../data/comparisons'
const faqSchema = {
'@context': 'https://schema.org',
@@ -411,6 +412,41 @@ export default function ComparePage() {
+
+ Specific alternatives
+
+ Compare OpenAdapt with a specific alternative.
+
+
+ Each page credits the alternative for what it genuinely
+ does well, then shows where OpenAdapt differs on
+ verification, outcomes, deployment, and openness.
+
+
+ {COMPARISON_LINKS.map((link) => (
+
+ track(EVENTS.COMPARE_CTA_CLICK, {
+ cta: `compare_${link.slug}`,
+ location: 'compare_alternatives',
+ })
+ }
+ >
+ {link.title}
+
+ ))}
+
+
+
diff --git a/pages/compare/[slug].js b/pages/compare/[slug].js
new file mode 100644
index 00000000..8acfd871
--- /dev/null
+++ b/pages/compare/[slug].js
@@ -0,0 +1,235 @@
+import Head from 'next/head'
+import Link from 'next/link'
+
+import Footer from '@components/Footer'
+import {
+ COMPARISONS,
+ COMPARISON_LINKS,
+ OPENADAPT_DIFFERENTIATORS,
+ QUALIFICATION_EVIDENCE_URL,
+} from '../../data/comparisons'
+
+// Targeted alternative pages under /compare/. The overview table stays
+// on /compare; these pages go deeper on one alternative at a time under the
+// honesty rules documented in data/comparisons.js: credit the alternative's
+// real strengths, differentiate only on what OpenAdapt actually does, and
+// never lean on commoditized capabilities (recording, visual targeting,
+// Citrix awareness, self-healing) as if they were unique.
+
+export async function getStaticPaths() {
+ return {
+ paths: COMPARISONS.map(({ slug }) => ({ params: { slug } })),
+ fallback: false,
+ }
+}
+
+export async function getStaticProps({ params }) {
+ const comparison = COMPARISONS.find(({ slug }) => slug === params.slug)
+ return { props: { comparison } }
+}
+
+export default function ComparisonDetailPage({ comparison }) {
+ const url = `https://openadapt.ai/compare/${comparison.slug}`
+ const webPageSchema = {
+ '@context': 'https://schema.org',
+ '@type': 'WebPage',
+ name: comparison.title,
+ url,
+ description: comparison.metaDescription,
+ isPartOf: {
+ '@type': 'WebSite',
+ name: 'OpenAdapt.AI',
+ url: 'https://openadapt.ai',
+ },
+ inLanguage: 'en',
+ }
+
+ return (
+
+
+
{`${comparison.title} | OpenAdapt`}
+
+
+
+
+
+
+
+
+
+
+
+ Automation decision guide
+
+
+
+ {comparison.title}
+
+
+ {comparison.intro}
+
+
+
+
+ {comparison.theirStrengths.heading}
+
+
+ {comparison.theirStrengths.items.map((item) => (
+
+ {item}
+
+ ))}
+
+
+
+
+
+ What OpenAdapt does differently
+
+
+ These are the differences that hold up under scrutiny.
+ Recording demonstrations, visual targeting, virtual
+ desktop awareness, and selector repair are broadly
+ available across modern tools and are not claimed here
+ as unique.
+
+
+ {OPENADAPT_DIFFERENTIATORS.map((item) => (
+
+
+ {item.title}
+
+
+ {item.body}
+
+
+ ))}
+
+
+ Per-surface acceptance results are published in the{' '}
+
+ qualification evidence
+
+ .
+
+
+
+
+
+ Which should you choose?
+
+
+
+
+ Choose {comparison.name} when
+
+
+ {comparison.chooseThem}
+
+
+
+
+ Choose OpenAdapt when
+
+
+ {comparison.chooseUs}
+
+
+
+
+ {comparison.honestNote}
+
+
+
+
+
+ Other comparisons
+
+
+
+
+ Overview: compare all approaches
+
+
+ {COMPARISON_LINKS.filter(
+ (link) => link.slug !== comparison.slug
+ ).map((link) => (
+
+
+ {link.title}
+
+
+ ))}
+
+
+
+
+
+ Test the difference on one real workflow.
+
+
+ Bring one repeated, consequential workflow and measure
+ authoring time, run time, intervention rate, and
+ incorrect-success rate against your current approach.
+
+
+
+
+
+
+
+ )
+}
diff --git a/pages/how-it-works.js b/pages/how-it-works.js
index 78919783..12fdcfd6 100644
--- a/pages/how-it-works.js
+++ b/pages/how-it-works.js
@@ -6,6 +6,7 @@ import DriftOutcomes from '@components/DriftOutcomes'
import Footer from '@components/Footer'
import HowItWorksCondensed from '@components/HowItWorksCondensed'
import ReferenceDemoShowcase from '@components/ReferenceDemoShowcase'
+import RemoteModesFigure from '@components/RemoteModesFigure'
import Reveal from '@components/Reveal'
// Concepts / method page. The homepage keeps one clear narrative arc, so the
@@ -103,6 +104,10 @@ export default function HowItWorksPage() {
+
+
+
+
+
+ Partner Program | OpenAdapt
+
+
+
+
+
+
+
+
+ Partner program
+
+ Bring verified execution to the customers you already
+ serve.
+
+
+ OpenAdapt compiles demonstrated GUI workflows into
+ deterministic, governed programs and verifies the business
+ effect out of band. Partners embed it in their products,
+ operate it for their clients, or deliver and deploy it
+ inside customer boundaries. The runtime is MIT-licensed;
+ the partnership adds commercial terms, qualification
+ method, and support boundaries.
+
+
+
+
+ Four partner tracks
+
+
+ {TRACKS.map((track) => (
+
+
+ {track.title}
+
+
+ {track.who}
+
+
+
+ Model:{' '}
+
+ {track.model}
+
+
+ Responsibility boundaries
+
+
+ {track.boundaries.map((boundary) => (
+
+ {boundary}
+
+ ))}
+
+
+ ))}
+
+
+
+
+ Program status
+
+ An application-based program, not a self-serve portal.
+
+
+ We review every application individually and start each
+ partnership from one concrete customer workflow, not a
+ logo exchange. There is no self-serve partner portal,
+ directory listing, or certification marketplace today.
+ Commercial terms (OEM, operations, and services) are
+ agreed per partner. Consequential workflows go live
+ only after qualification on the exact application and
+ environment, per the published{' '}
+
+ qualification evidence
+
+ .
+
+
+
+
+
+ Apply to the partner program
+
+
+ Tell us which track fits and what you would build or
+ operate. If a specific end-customer workflow is already
+ in view, the fastest path is to also{' '}
+
+ qualify that workflow
+
+ .
+
+
+
+
+
+
+
+ )
+}
diff --git a/public/form.html b/public/form.html
index 74472463..f78e22a3 100644
--- a/public/form.html
+++ b/public/form.html
@@ -51,10 +51,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Apply to partner
+
+
diff --git a/public/llms.txt b/public/llms.txt
index 52e692b5..76a5606f 100644
--- a/public/llms.txt
+++ b/public/llms.txt
@@ -14,6 +14,13 @@
## Core Concepts
- [How It Works](https://openadapt.ai/#how-it-works): Record, compile, replay, resolve or halt, then verify and report
- [Compare Approaches](https://openadapt.ai/compare): When to choose OpenAdapt, traditional RPA, computer-use agents, browser recorders, or a direct API
+- [OpenAdapt vs UiPath](https://openadapt.ai/compare/uipath): Honest comparison with the leading enterprise RPA platform
+- [OpenAdapt vs Microsoft Power Automate](https://openadapt.ai/compare/power-automate): Honest comparison with Microsoft ecosystem RPA
+- [OpenAdapt vs computer-use agents](https://openadapt.ai/compare/computer-use-agents): Deterministic verified replay compared with per-run model agents
+- [OpenAdapt vs record-and-replay tools](https://openadapt.ai/compare/record-and-replay): What governance adds beyond demonstration-based authoring
+- [OpenAdapt vs browser-agent platforms](https://openadapt.ai/compare/browser-agents): Cross-surface verified execution compared with web-agent stacks
+- [OpenAdapt vs hand-rolled scripts](https://openadapt.ai/compare/hand-rolled-scripts): What governed verification adds beyond Playwright or Selenium scripts
+- [Partner Program](https://openadapt.ai/partners): Application-based tracks for vertical software/OEM, RCM and BPO operators, integration/services, and MSP/deployment partners
- [How It Runs](https://openadapt.ai/#product-status): Governed workflow lifecycle and local, managed cloud, or customer-controlled execution
- [FAQ](https://openadapt.ai/#faq): How OpenAdapt differs from RPA tools and AI computer-use agents, data locality, licensing
- [Security and Data Boundaries](https://openadapt.ai/security): Screenshot, model, secret, report, certification, and commercial deployment boundaries
diff --git a/public/sitemap.xml b/public/sitemap.xml
index 75e42979..7bf8fc88 100644
--- a/public/sitemap.xml
+++ b/public/sitemap.xml
@@ -138,6 +138,48 @@
monthly
0.7
+
+ https://openadapt.ai/partners
+ 2026-07-26
+ monthly
+ 0.7
+
+
+ https://openadapt.ai/compare/uipath
+ 2026-07-26
+ monthly
+ 0.7
+
+
+ https://openadapt.ai/compare/power-automate
+ 2026-07-26
+ monthly
+ 0.7
+
+
+ https://openadapt.ai/compare/computer-use-agents
+ 2026-07-26
+ monthly
+ 0.7
+
+
+ https://openadapt.ai/compare/record-and-replay
+ 2026-07-26
+ monthly
+ 0.7
+
+
+ https://openadapt.ai/compare/browser-agents
+ 2026-07-26
+ monthly
+ 0.7
+
+
+ https://openadapt.ai/compare/hand-rolled-scripts
+ 2026-07-26
+ monthly
+ 0.7
+
https://openadapt.ai/privacy-policy
2026-05-25
diff --git a/tests/comparisonPages.test.js b/tests/comparisonPages.test.js
new file mode 100644
index 00000000..2a154a99
--- /dev/null
+++ b/tests/comparisonPages.test.js
@@ -0,0 +1,150 @@
+const assert = require('node:assert/strict')
+const fs = require('node:fs')
+const path = require('node:path')
+const test = require('node:test')
+
+const root = path.join(__dirname, '..')
+const read = (relativePath) =>
+ fs.readFileSync(path.join(root, relativePath), 'utf8')
+
+const dataSource = read('data/comparisons.js')
+const detailPage = read('pages/compare/[slug].js')
+const overview = read('pages/compare.js')
+const llms = read('public/llms.txt')
+const sitemap = read('public/sitemap.xml')
+
+// The data module is plain ESM with no JSX, so it can be imported directly.
+const load = () => import('../data/comparisons.js')
+
+const EXPECTED_SLUGS = [
+ 'uipath',
+ 'power-automate',
+ 'computer-use-agents',
+ 'record-and-replay',
+ 'browser-agents',
+ 'hand-rolled-scripts',
+]
+
+test('every targeted comparison page exists with complete, structured content', async () => {
+ const { COMPARISONS } = await load()
+ assert.deepEqual(
+ COMPARISONS.map(({ slug }) => slug).sort(),
+ [...EXPECTED_SLUGS].sort()
+ )
+ for (const comparison of COMPARISONS) {
+ assert.ok(comparison.title.startsWith('OpenAdapt vs'), comparison.slug)
+ assert.ok(comparison.metaDescription.length > 60, comparison.slug)
+ assert.ok(comparison.intro.length > 100, comparison.slug)
+ assert.ok(
+ comparison.theirStrengths.items.length >= 4,
+ `${comparison.slug} credits the alternative with at least four real strengths`
+ )
+ assert.ok(comparison.chooseThem.length > 80, comparison.slug)
+ assert.ok(comparison.chooseUs.length > 80, comparison.slug)
+ assert.ok(comparison.honestNote.length > 80, comparison.slug)
+ }
+})
+
+test('OpenAdapt differentiates only on the real axes, never commoditized ones', async () => {
+ const { OPENADAPT_DIFFERENTIATORS } = await load()
+ const titles = OPENADAPT_DIFFERENTIATORS.map(({ title }) => title)
+ for (const required of [
+ 'Independent business-effect verification',
+ 'Explicit transaction outcomes',
+ 'Deterministic healthy runs',
+ 'External zero-install remote lane',
+ 'Customer-controlled sensitive data',
+ 'Open MIT local runtime',
+ 'Published qualification evidence',
+ ]) {
+ assert.ok(titles.includes(required), `differentiator: ${required}`)
+ }
+
+ // Commoditized capabilities must never be claimed as unique. Recording,
+ // visual targeting, Citrix awareness, and self-healing are widely
+ // available; the copy may mention them only to disclaim them. Comments
+ // quote the banned phrases as guidance, so strip them first.
+ const rendered = dataSource
+ .replace(/^\s*\/\/.*$/gm, '')
+ .replace(/\/\*[\s\S]*?\*\//g, '')
+ assert.doesNotMatch(rendered, /self[- ]heal/i)
+ assert.doesNotMatch(rendered, /only (tool|platform|product) that/i)
+ assert.doesNotMatch(rendered, /unique(ly)? (able|capable)/i)
+ assert.match(
+ rendered,
+ /not differentiators|not claimed here|does not claim to record better|commodity/,
+ 'copy explicitly disclaims commoditized capabilities'
+ )
+})
+
+test('competitor pricing stays within well-known published figures, hedged', () => {
+ // The only currency figures allowed anywhere in the comparison content
+ // are Power Automate's widely published price points, and each must be
+ // hedged with "around" plus a "published pricing" frame.
+ const dollarFigures = dataSource.match(/\$\d[\d,]*/g) || []
+ assert.deepEqual([...new Set(dollarFigures)].sort(), ['$15', '$215'])
+ assert.match(dataSource, /published pricing/i)
+ assert.match(dataSource, /around \$15 per user per month/)
+ assert.match(dataSource, /\$215 per bot per month/)
+ assert.match(dataSource, /roughly \$215/)
+})
+
+test('external-lane claims carry the honest qualification status', () => {
+ // Mirrors public/status.json: the external lane is qualified against a
+ // deterministic stand-in (and a real FreeRDP round trip); real ICA/HDX
+ // is qualified per customer before consequential use.
+ for (const [label, source] of [
+ ['comparisons data', dataSource],
+ ['remote modes figure', read('components/RemoteModesFigure.js')],
+ ]) {
+ assert.match(source, /stand-in/, `${label} discloses the stand-in`)
+ assert.match(source, /ICA\/HDX/, `${label} names the pending scope`)
+ assert.match(
+ source,
+ /qualified per customer/,
+ `${label} states per-customer qualification`
+ )
+ }
+})
+
+test('detail pages render both sides and route from the overview page', () => {
+ assert.match(detailPage, /getStaticPaths/)
+ assert.match(detailPage, /theirStrengths/)
+ assert.match(detailPage, /What OpenAdapt does differently/)
+ assert.match(detailPage, /Choose \{comparison\.name\} when/)
+ assert.match(detailPage, /Choose OpenAdapt when/)
+ assert.match(detailPage, /honestNote/)
+
+ assert.match(overview, /COMPARISON_LINKS/)
+ assert.match(overview, /Compare OpenAdapt with a specific alternative\./)
+})
+
+test('machine-readable discovery includes every comparison route', () => {
+ for (const slug of EXPECTED_SLUGS) {
+ assert.match(
+ llms,
+ new RegExp(`https://openadapt\\.ai/compare/${slug}`),
+ `llms.txt lists /compare/${slug}`
+ )
+ assert.match(
+ sitemap,
+ new RegExp(`https://openadapt\\.ai/compare/${slug}`),
+ `sitemap lists /compare/${slug}`
+ )
+ }
+})
+
+test('comparison and partner surfaces contain no em dashes', () => {
+ for (const relativePath of [
+ 'data/comparisons.js',
+ 'pages/compare/[slug].js',
+ 'pages/partners.js',
+ 'components/PartnerInquiryForm.js',
+ 'components/RemoteModesFigure.js',
+ ]) {
+ assert.ok(
+ !read(relativePath).includes('—'),
+ `${relativePath} has no em dashes`
+ )
+ }
+})
diff --git a/tests/leadPrivacy.test.js b/tests/leadPrivacy.test.js
new file mode 100644
index 00000000..ff42bbbb
--- /dev/null
+++ b/tests/leadPrivacy.test.js
@@ -0,0 +1,110 @@
+const assert = require('node:assert/strict')
+const fs = require('node:fs')
+const path = require('node:path')
+const test = require('node:test')
+
+const root = path.join(__dirname, '..')
+const read = (relativePath) =>
+ fs.readFileSync(path.join(root, relativePath), 'utf8')
+
+// Lead data must never appear in a URL: not in the fetch target, not in a
+// query string, and not through a native GET fallback if JavaScript fails
+// mid-submit. Complemented by the browser-level check in
+// cypress/e2e/lead-privacy.cy.js.
+
+const LEAD_FORMS = [
+ 'components/WorkflowQualificationForm.js',
+ 'components/PartnerInquiryForm.js',
+ 'components/ContributorProgramForm.js',
+]
+
+test('lead forms submit through a POST body, never a URL', () => {
+ for (const relativePath of LEAD_FORMS) {
+ const source = read(relativePath)
+
+ // The durable Netlify path is always a bare '/form.html' with the
+ // encoded fields in the request body.
+ assert.match(source, /fetch\('\/form\.html'/, relativePath)
+ assert.match(source, /method: 'POST'/, relativePath)
+ assert.match(
+ source,
+ /body: (data|formData)\.toString\(\)/,
+ relativePath
+ )
+
+ // Never a query string on the submission target.
+ assert.doesNotMatch(source, /form\.html\?/, relativePath)
+ assert.doesNotMatch(source, /[?&]email=/, relativePath)
+
+ // Never a client-side navigation that could carry lead fields.
+ assert.doesNotMatch(
+ source,
+ /window\.location\s*=|location\.href\s*=|router\.push\(`[^`]*\$\{/,
+ relativePath
+ )
+
+ // No action attribute: combined with an explicit POST method there
+ // is no native fallback that serializes fields into a query string.
+ assert.doesNotMatch(source, /]*\baction=/s, relativePath)
+ }
+
+ // The two forms this change owns also declare method="POST" in markup so
+ // even a JS-less native submit cannot GET lead fields into a URL.
+ for (const relativePath of [
+ 'components/WorkflowQualificationForm.js',
+ 'components/PartnerInquiryForm.js',
+ ]) {
+ assert.match(read(relativePath), /method="POST"/, relativePath)
+ }
+})
+
+test('qualification analytics events carry no lead fields', () => {
+ const source = read('components/WorkflowQualificationForm.js')
+ // Funnel events send routing metadata only; asserting the exact payloads
+ // keeps emails, names, and workflow text out of analytics URLs.
+ assert.match(
+ source,
+ /track\(EVENTS\.QUALIFICATION_FORM_START, \{ location: [^}]*\}\)/
+ )
+ assert.match(
+ source,
+ /track\(EVENTS\.QUALIFICATION_FORM_SUBMIT, \{\s*tier: qualification\.tier,\s*location: [^}]*\}\)/
+ )
+ assert.doesNotMatch(source, /track\([^)]*form\.email/s)
+ assert.doesNotMatch(source, /track\([^)]*form\.name/s)
+})
+
+test('the qualification intake captures consent and lead segment durably', () => {
+ const source = read('components/WorkflowQualificationForm.js')
+ const definitions = read('public/form.html')
+
+ // Required, privacy-policy-linked consent.
+ assert.match(
+ source,
+ /name="privacyConsent"[\s\S]{0,200}required/,
+ 'consent checkbox is required'
+ )
+ assert.match(source, /href="\/privacy-policy"/)
+
+ // Captured lead segment with the five audience values.
+ for (const segment of [
+ 'enterprise_operator',
+ 'oem_vertical',
+ 'bpo_services',
+ 'developer',
+ 'community',
+ ]) {
+ assert.ok(
+ source.includes(`value="${segment}"`),
+ `lead segment option ${segment}`
+ )
+ }
+
+ // Both fields are registered in the durable Netlify definition, or the
+ // values would be silently dropped from submissions.
+ const qualificationDefinition = definitions
+ .split('name="workflow-qualification"')[1]
+ .split(' ')[0]
+ assert.match(qualificationDefinition, /name="leadSegment"/)
+ assert.match(qualificationDefinition, /name="privacyConsent"/)
+})
diff --git a/tests/partnersProgram.test.js b/tests/partnersProgram.test.js
new file mode 100644
index 00000000..56b0e0c8
--- /dev/null
+++ b/tests/partnersProgram.test.js
@@ -0,0 +1,98 @@
+const assert = require('node:assert/strict')
+const fs = require('node:fs')
+const path = require('node:path')
+const test = require('node:test')
+
+const root = path.join(__dirname, '..')
+const read = (relativePath) =>
+ fs.readFileSync(path.join(root, relativePath), 'utf8')
+
+const page = read('pages/partners.js')
+const intake = read('components/PartnerInquiryForm.js')
+const formDefinitions = read('public/form.html')
+const nav = read('components/NavHeader.js')
+const footer = read('components/Footer.js')
+
+const TRACK_VALUES = [
+ 'vertical_oem',
+ 'rcm_bpo',
+ 'integration_services',
+ 'msp_deployment',
+]
+
+test('partner leads use a dedicated Netlify form, segmented from contact', () => {
+ // The runtime submit and the build-time definition must agree on the
+ // form name, or Netlify silently drops submissions.
+ assert.match(intake, /formData\.set\('form-name', 'partner-inquiry'\)/)
+ assert.match(intake, /fetch\('\/form\.html'/)
+ assert.match(
+ formDefinitions,
+ /')[0]
+ assert.ok(
+ new RegExp(`name="${field}"`).test(partnerDefinition),
+ `form.html registers partner field ${field}`
+ )
+ }
+ assert.match(page, /PartnerInquiryForm/)
+})
+
+test('the track selector matches the tracks presented on the page', () => {
+ for (const value of TRACK_VALUES) {
+ assert.ok(
+ intake.includes(`value: '${value}'`),
+ `intake offers track ${value}`
+ )
+ assert.ok(page.includes(`id: '${value}'`), `page presents ${value}`)
+ }
+})
+
+test('each track states its model and responsibility boundaries', () => {
+ for (const heading of [
+ 'Vertical software / OEM',
+ 'RCM and BPO operators',
+ 'Integration and services partners',
+ 'MSP and deployment partners',
+ ]) {
+ assert.ok(page.includes(heading), `track: ${heading}`)
+ }
+ assert.match(page, /model:/)
+ assert.match(page, /boundaries: \[/)
+ assert.match(page, /Responsibility boundaries/)
+ // Every track spells out the three boundary dimensions.
+ assert.equal((page.match(/Qualification:/g) || []).length, 4)
+ assert.equal((page.match(/Support:/g) || []).length, 4)
+ assert.equal((page.match(/Packs:/g) || []).length, 4)
+})
+
+test('the program is status-honest: apply, not self-serve', () => {
+ assert.match(page, /not a self-serve portal/)
+ assert.match(page, /review every application individually/i)
+ assert.doesNotMatch(page, /partner portal today is live|sign up now/i)
+ assert.match(intake, /Apply to partner/)
+})
+
+test('partners is reachable from the primary nav and the footer', () => {
+ assert.ok(nav.includes(`{ label: 'Partners & OEM', href: '/partners' }`))
+ assert.ok(footer.includes('href="/partners"'))
+ // Community path: the footer offers the contribute program.
+ assert.ok(
+ footer.includes(`{ label: 'Contribute workflows', href: '/contribute' }`)
+ )
+})