# NexusCRM HubSpot Acceptance

This private repository is the single source of truth for PingCAP NexusCRM
HubSpot acceptance cases and their Node.js runner. It is a QA/release-evidence
boundary, not an L0, L1, or L2 runtime repository.

The initial 132-case catalog was imported from
`pingcap-inc/nexuscrm-pingcap-pack@b2394034cd7ad4c86daf72e22c26494d5cb7dc2f`.
After the 2026-08-26 case-by-case audit, 51 obsolete, duplicate, stopped-path,
or non-executable cases were retired. This repair round moved one Salesforce
Tooling parity case outside the HubSpot boundary and added five executable
historical-regression/health cases. A separate provider-only harness smoke and
two fixed Set Campaign canaries now bring the current catalog to 88 cases. Exact
import provenance and both catalog digests are recorded in
[`source-lock.json`](source-lock.json).

## Ownership boundary

- This repository owns `config/cases/hubspot/**`, `config/roles.json`, the
  runner, catalog validation, and test reports.
- `pingcap-inc/nexuscrm-pingcap-pack` remains authoritative for L2 metadata,
  deployment topology, active-slot preparation, release/BOM identity, and
  environment-specific operator scripts.
- Source-backed `hubspot_contract`, TiDB, and pressure cases require an explicit
  `NEXUSCRM_L2_ROOT`; the runner never guesses a sibling checkout.
- Secrets must not be committed here. Live orchestration stays in the L2
  workflow, which owns the target credentials and self-hosted runners.
- Acceptance `main` is the only case ref admitted by the L2 live workflow. Each
  job resolves it once, validates the catalog, and records the exact checkout
  SHA and catalog digest in the report. Merging a case here therefore makes it
  the default for later L2 runs without a separate L2 pin-update PR.
- Because Acceptance `main` is a credentialed-execution admission boundary,
  changes must cross its protected pull-request and provider-free CI checks;
  L2 does not accept an arbitrary Acceptance branch or ref input.

## CI

`Validate HubSpot Acceptance Catalog` runs on every pull request and push to
`main`. It verifies:

1. all 88 current case files are referenced exactly once;
2. case IDs are unique and case kinds are supported;
3. every case has one supported governance class and no unclassified optional
   case can enter the catalog;
4. the current catalog digest matches `source-lock.json` without rewriting the
   immutable 132-case import record;
5. all runner unit tests pass; and
6. full dry-run discovery succeeds for `staging`, `test1`, `test2`, and `test3`.

The routine functionality suite has 64 release-required cases. Its 24 excluded
cases consist of 12 targeted must-pass cases, three Set Campaign performance
cases, and the nine-cell inbound pressure matrix. The catalog's `optional`
flag means excluded from routine functionality; the selected suite's
`performanceContract.expectation` determines whether a performance case is
required.

The `set-campaign-performance` group runs serially in this order:

- 20 contacts: required (`must_pass`);
- 100 contacts: required (`must_pass`);
- 2,000 contacts: optional (`should_pass`, `releaseBlocking=false`).

A failed, blocked, skipped, or unstarted required case fails the performance
acceptance gate. The optional 2,000-contact result retains its exact
correctness assertions and actual status, but does not fail that gate when
both required cases pass. Selecting only the 2,000-contact case still exits
nonzero on failure. `summary.acceptanceStatus` records the gate verdict;
`summary.failed` still counts every failed case, including optional ones.

The L2 workflow passes `--suite-timeout-ms 2580000`, sharing one 43-minute
execution budget across all three cases and reserving report time inside its
45-minute execution step. Each case uses the smaller of its existing deadline
and the suite time remaining. An exhausted budget prevents the next case from
starting and records a non-PASS; suite scheduling never extends a case limit. Functionality
execution budgets are unchanged.

The required 20-contact case allows up to ten minutes for deep consistency
within a 15-minute case ceiling. This accommodates the late mapped-field
convergence observed in Staging run 33997497645 while retaining the 60-second
stability delay and final fingerprint check. Earlier phases and final readback
share that case budget; exhaustion still fails. The shared 43-minute suite
limit and the 100- and 2,000-contact case budgets are unchanged.

Every live invocation must execute at least one case. Empty selections or runs
that skip every case exit nonzero, even when all selected cases are optional;
`--dry-run` remains the explicit mode for planning without execution.

To render the complete current classification directly from case metadata:

```bash
node scripts/case-inventory.mjs
```

The audit rationale and retired-case ledger are in
[`docs/case-audit-2026-08-26.md`](docs/case-audit-2026-08-26.md).

`HubSpot Target Dry Run` is manually dispatchable with a selectable target and
`smoke`, `full`, or `custom` suite mode. It performs no remote writes and needs
no target credentials.

The routine live suite ends with the required `suite-durable-health` post-run
gate. Provider-facing assertions cannot hide an `uncertain` Integration
command, a later same-link command blocked behind that poison, or run-created
WorkItem/Runtime PlatformCommand residue. D3 inbound terminal oracles follow
`ConnectorWorkItem -> ConnectorRuntimeHandoffReceipt -> RuntimePlatformCommand`;
legacy `ConnectorEvent.Status` is not treated as execution authority.

## Local verification

```bash
npm test --prefix runner
node scripts/validate-catalog.mjs
STAGING_INSPECTION_TARGET_ENVIRONMENT=staging \
  HUBSPOT_SUITE_MODE=full \
  node runner/src/cli.mjs --dry-run --case-concurrency 4
```

For a source-backed or live case, point to an exact L2 checkout:

```bash
NEXUSCRM_L2_ROOT=/absolute/path/to/nexuscrm-pingcap-pack \
  node runner/src/cli.mjs --case admin/hubspot/<case-id>
```

Never run live cases against Production. The runner admits only the declared
non-Production target matrix and HubSpot mutations require an exact Sandbox
portal check.
