# Test a migrated app three ways

The scary part of a migration is not generating the new project — it is knowing whether the result actually behaves before you point anything real at it. HS-X makes the freshly-migrated dupe invocable through the production runtime router on your machine, from whichever client you think in: your terminal, your coding agent, or a bare HTTP request.

**TL;DR** — `hs-x dev invoke <capability-id>` runs one capability through the production router with fixture defaults; `--object`/`--input`/`--install` override them, `--last` replays the previous dispatch. Agents get the same engine as MCP tools (`hsx.dev.capabilities`, `hsx.dev.invoke`, `hsx.dev.invoke_last`). A running `hs-x dev` server exposes it to any HTTP client at `POST /_hsx/invoke/<capability-id>`.

## One engine, three doors

`hs-x migrate run` leaves you with a net-new project — card-called legacy serverless functions as card backends, other portable functions as tools, webhook subscriptions as triggers, and cards carried over with their React source. The question that matters next: does each one do what the legacy version did?

The answer mechanism is deliberately boring. There is exactly one invocation engine, and it is not a test double: your worker modules are loaded as-is, and the dispatch goes through the same runtime router a deployed Worker serves — the same payload validation, the same handler context, the same result envelope. The three doors into that engine only differ in who is knocking:

- **The terminal**, for you: `hs-x dev invoke`.
- **MCP**, for your coding agent: `hsx.dev.*` tools.
- **HTTP**, for scripts and CI: `POST /_hsx/invoke/<id>` on the dev server.

Because the engine is the production path, a green invocation here means the handler, its types, and its envelope survive contact with the runtime — not that they passed a simulation of it.

## The card-to-backend seam is wired

A migrated serverless function is still a scaffold, not a completed body port. `hs-x migrate run` reports the remaining `TODO(migration)` markers. Shared helpers from the legacy function `lib/` directory are preserved under `src/workers/legacy/lib/`; update imports to that path as you port each body.

For project cards, migration classifies each statically named `runServerlessFunction` call as a `worker.cardBackend`, infers its input fields from the frontend `parameters` object and legacy `context.parameters` reads, and generates `_hsx-migration-run-serverless.ts`. The card's provider line is changed to pass that compatibility function down, so existing hooks remain untouched:

```tsx
const result = await runServerlessFunction({
  name: 'get-orders',
  parameters: { contactId: context.crm.objectId },
});
```

The shim posts `{ input: parameters }` through `hubspot.fetch` to `/_hsx/cards/<name>` and translates the runtime envelope back to the legacy `{ status, message, response }` contract. At generation time, the Worker origin is a visible placeholder in both the shim and `permittedUrls.fetch`; `hs-x deploy` replaces it with the resolved workers.dev origin before HubSpot upload. A `project-card.serverless-call.routed` finding confirms the calls that were connected. If migration cannot recognize the provider shape safely, it leaves the source unchanged and emits `project-card.serverless-call.rewire` for that card.

Paste or port the original function body into the generated card backend, change legacy `context.parameters.foo` reads to `input.foo`, and then exercise the complete card-to-worker request in `hs-x dev`. A green structural `hs-x check` proves the scaffold and linkage are valid; the body TODOs still mark the work required for behavioral parity.

## The terminal

From the migrated project root (or anywhere, with `--cwd`):

```bash
hs-x dev invoke score-record \
  --object '{"id":"d1","objectType":"deals","properties":{"amount":"50000"}}' \
  --input '{"threshold":25000}'
```

Fixture defaults fill anything you do not pass — a bare `hs-x dev invoke score-record` works seconds after migration. The output is the runtime's own envelope, pretty-printed; exit code 0 means the dispatch and the handler both succeeded, so it slots straight into a shell loop or a pre-deploy check.

When an invocation fails, fix the handler and re-run the exact same dispatch:

```bash
hs-x dev invoke --last
```

Every invocation records its capability id and full payload to `.hs-x/last-invocation.json`; `--last` replays it, and explicit flags override individual parts. Typos get caught with the full menu: an unknown capability id error lists every id the project actually declares.

## The agent

The HS-X MCP server exposes the same engine to coding agents. Three tools carry the loop:

- `hsx.dev.capabilities` — list every worker and capability id in the project, with kinds. This is the discovery step, so the agent never guesses ids.
- `hsx.dev.invoke` — dispatch one capability; `input`, `enrolledObject`, and `install` are optional structured arguments over the same fixture defaults.
- `hsx.dev.invoke_last` — replay the recorded dispatch after an edit.

That triad is a complete debug cycle: discover, invoke, read the envelope, edit the handler, replay. Pointed errors flow through verbatim — an agent that asks for a capability that does not exist is told what does, in the same message.

## Raw HTTP

Start the dev server and every capability gets an HTTP door:

```bash
hs-x dev --port 8787
```

```bash
curl -s -X POST http://127.0.0.1:8787/_hsx/invoke/score-record \
  -H 'content-type: application/json' \
  -d '{"input":{"threshold":25000}}'
```

The body takes the same optional `input` / `enrolledObject` / `install` keys; the response wraps the runtime envelope with the worker and kind that served it. This is the door for CI smoke jobs, REST clients, and anything else that speaks HTTP and nothing else.

## What fixtures cover, and what they do not

Fixture invocation proves the handler logic, the capability wiring, and the runtime contract. Two things it deliberately does not fake:

- **HubSpot API calls.** A handler that uses `ctx.hubspot` needs a connected portal install to authenticate as — the same requirement production has. Run the full `hs-x dev` session with a connected account for that leg.
- **Pre-cutover decisions.** Migration findings like a legacy webhook receiver URL or an endpoint function's public contract are listed by `hs-x migrate run` and stay visible in the generated handler stubs. Testing does not make those decisions for you; it tells you the code around them holds.

**Where next**

- [Migrate an app: the one-command flow that produces the dupe](/docs/guides/migrate)
- [Dev mode: log streaming, portal overrides, UI extensions](/docs/guides/dev-mode)
- [Agent tools: expose your own capabilities to agents](/docs/guides/agent-tools)

