Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
run: pnpm install

- name: Run benchmarks
run: cd packages/bridge && CI=true node --experimental-transform-types --conditions source bench/engine.bench.ts > bench-results.json 2>/dev/null
run: cd packages/bridge && CI=true node --experimental-transform-types bench/engine.bench.ts > bench-results.json 2>/dev/null

- name: Upload benchmark results
uses: actions/upload-artifact@v4
Expand Down Expand Up @@ -75,7 +75,7 @@ jobs:
run: pnpm install

- name: Run benchmarks
run: cd packages/bridge && CI=true node --experimental-transform-types --conditions source bench/engine.bench.ts > bench-results.json 2>/dev/null
run: cd packages/bridge && CI=true node --experimental-transform-types bench/engine.bench.ts > bench-results.json 2>/dev/null

- uses: bencherdev/bencher@main

Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ playground/ Browser playground (Vite + React)

**Run a single test file:**
```bash
node --experimental-transform-types --conditions source --test test/<filename>.test.ts
node --experimental-transform-types --test test/<filename>.test.ts
```

Tests are **co-located with each package**. The main test suites:
Expand All @@ -77,7 +77,7 @@ Tests are **co-located with each package**. The main test suites:

- **ESM** (`"type": "module"`) with `.ts` import extensions (handled by `rewriteRelativeImportExtensions`)
- **Strict mode** — `noUnusedLocals`, `noUnusedParameters`, `noImplicitReturns`, `noFallthroughCasesInSwitch`
- **Dev running:** `--experimental-transform-types --conditions source`
- **Dev running:** `--experimental-transform-types`
- **Path mappings:** `tsconfig.base.json` maps `@stackables/*` for cross-package imports

## Deep-dive docs
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,11 @@ const schema = bridgeTransform(createSchema({ typeDefs }), instructions, {
```

**[Read the Tools & Extensions Guide](https://bridge.sdk42.com/advanced/custom-tools/)**

## Testing Prompt

The reason we write tests is to catch bugs so we can fix them — not to document broken behavior and ship it.

We never hide problems or avoid broken scenarios to make tests pass.

It is always better to not ship and have broken tests than to break our users trust.
6 changes: 3 additions & 3 deletions docs/fuzz-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,9 @@ When a fuzz run finds a new issue:
pnpm test

# Single fuzz file
node --experimental-transform-types --conditions source --test packages/bridge-compiler/test/fuzz-runtime-parity.test.ts
node --experimental-transform-types --conditions source --test packages/bridge/test/fuzz-parser.test.ts
node --experimental-transform-types --conditions source --test packages/bridge-stdlib/test/fuzz-stdlib.test.ts
node --experimental-transform-types --test packages/bridge-compiler/test/fuzz-runtime-parity.test.ts
node --experimental-transform-types --test packages/bridge/test/fuzz-parser.test.ts
node --experimental-transform-types --test packages/bridge-stdlib/test/fuzz-stdlib.test.ts

# Reproduce a specific failing seed
# Add { seed: -1234567, path: "0", endOnFailure: true } to fc.assert options
Expand Down
2 changes: 1 addition & 1 deletion docs/profiling.md
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ Use the focused profiling target instead:
```bash
# Runs a single scenario in a tight loop — cleaner profiles
BRIDGE_PROFILE_FILTER="flat array 1000" BRIDGE_PROFILE_ITERATIONS=10000 \
node --experimental-transform-types --conditions source \
node --experimental-transform-types \
--cpu-prof --cpu-prof-dir profiles --cpu-prof-interval 50 \
scripts/profile-target.mjs
```
Expand Down
209 changes: 209 additions & 0 deletions docs/test-migration-playbook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
# Test Migration Playbook: Legacy → regressionTest

Migrate `packages/bridge/test/legacy/*.test.ts` to the `regressionTest` framework.

## Prerequisites

- Read `packages/bridge/test/utils/regression.ts` (the framework — DO NOT EDIT)
- Read `packages/bridge/test/utils/bridge-tools.ts` (test multitools)
- Study `packages/bridge/test/coalesce-cost.test.ts` as the gold-standard example

## Step-by-step process

### 1. Categorise every test in the legacy file

Read the file and sort each test into one of these buckets:

| Bucket | Action |
| ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| **Parser-only** (parses AST, checks wire structure) | DELETE — regressionTest's `parse → serialise → parse` covers this automatically |
| **Serializer roundtrip** (parse → serialize → parse) | DELETE — regressionTest does this automatically |
| **Runtime execution** (runs bridge, asserts data/errors) | MIGRATE to `regressionTest` scenarios |
| **Non-runtime tests** (class constructors, pure unit tests) | MOVE to the corresponding package test dir (e.g. `bridge-core/test/`, `bridge-parser/test/`) |
| **Tests requiring custom execution** (AbortSignal, custom contexts) | Keep using `forEachEngine` in the new file |

### 2. Design bridges for regressionTest

Group related runtime-execution tests into **logical regressionTest blocks**. Each block has:

```typescript
regressionTest("descriptive name", {
bridge: `
version 1.5
bridge Operation.field {
with test.multitool as a
with input as i
with output as o
// ... wires
}
`,
tools, // import { tools } from "./utils/bridge-tools.ts"
scenarios: {
"Operation.field": {
"scenario name": { input: {...}, assertData: {...}, assertTraces: N },
},
},
});
```

**Design rules:**

- One regressionTest can have **multiple bridges** (multiple operations in scenarios)
- Group by **feature/behavior** (e.g. "throw control flow", "continue/break in arrays")
- Each bridge needs enough scenarios to achieve **traversal coverage** (all non-error paths hit)
- Keep bridge definitions minimal — test one concept per wire

### 3. Use test.multitool everywhere possible

The multitool (`with test.multitool as a`) is a passthrough: input → output (minus `_`-prefixed keys).

**Capabilities:**

- `_error`: `input: { a: { _error: "boom" } }` → tool throws `Error("boom")`
- `_delay`: `input: { a: { _delay: 100, name: "A" } }` → delays 100ms, returns `{ name: "A" }`
- All other `_` keys are stripped from output
- Correctly handles nested objects and arrays

**Wiring pattern:**

```
a <- i.a // sends i.a as input to tool, tool returns cleaned copy
o.x <- a.y // reads .y from tool output
```

**Only use custom tool definitions when:**

- You need a tool that transforms data (not passthrough)
- You need AbortSignal handling on the tool side
- You need `ctx.signal` inspection

### 4. Write scenarios

Each scenario needs:

| Field | Required | Description |
| ---------------- | -------- | ----------------------------------------------------------------------- |
| `input` | Yes | Input object passed to bridge |
| `assertTraces` | Yes | Number of tool calls (or function for custom check) |
| `assertData` | No | Expected output data (object or function) |
| `assertError` | No | Expected error (regex or function) — mutually exclusive with assertData |
| `fields` | No | Restrict which output fields are resolved |
| `context` | No | Context values (for `with context as ctx`) |
| `tools` | No | Per-scenario tool overrides |
| `allowDowngrade` | No | Set `true` if compiler can't handle this bridge feature |
| `assertGraphql` | No | GraphQL-specific expectations (object or function) |
| `assertLogs` | No | Log assertions |

**assertData shorthand:** For simple cases, use object literal:

```typescript
assertData: { name: "Alice", age: 30 }
```

**assertError with regex:** Matches against `${error.name} ${error.message}`:

```typescript
assertError: /BridgeRuntimeError/; // matches error name
assertError: /name is required/; // matches error message
assertError: /BridgePanicError.*fatal/; // matches both
```

**assertError with function** (for instanceof checks):

```typescript
assertError: (err: any) => {
assert.ok(err instanceof BridgePanicError);
assert.equal(err.message, "fatal");
};
```

**fields for isolating wires:** When one wire throws but others don't, use `fields` to test them separately:

```typescript
"error on fieldA only": {
input: { ... },
fields: ["fieldA"], // only resolve this field
assertError: /message/,
assertTraces: 0,
},
```

### 5. Handle traversal coverage

The framework automatically checks that all non-error traversal paths are covered. Common uncovered paths:

- **empty-array**: Add a scenario with an empty array: `input: { a: { items: [] } }`
- **Fallback paths**: Add a scenario where each fallback fires
- **Short-circuit paths**: Add scenarios for each branch of ||/?? chains

If traversal coverage fails, the error message tells you exactly which paths are missing.

### 6. Handle compiler downgrade

The compiled engine doesn't support all features. When the compiler downgrades, add `allowDowngrade: true` to the scenario. Common triggers:

- `?.` (safe execution modifier) without `catch`
- Some complex expressions
- Certain nested array patterns

**Important:** `allowDowngrade` applies per-scenario, but the bridge is shared. If ANY wire in the bridge triggers downgrade, ALL scenarios need `allowDowngrade: true`.

### 7. Handle errors in GraphQL

as graphql has partial errors then we need to assert it separately

```typescript
assertGraphql: {
fieldA: /error message/i, // expect GraphQL error for this field
fieldB: "fallback-value", // expect this value
}
```

### 8. Move non-runtime tests

Tests that don't invoke the bridge execution engine belong in the corresponding package:

| Test type | Target |
| ------------------------ | -------------------------------------------------- |
| Error class constructors | `packages/bridge-core/test/execution-tree.test.ts` |
| Parser AST structure | `packages/bridge-parser/test/` |
| Serializer output format | `packages/bridge-parser/test/` |
| Type definitions | `packages/bridge-types/test/` |

### 9. Final verification

```bash
pnpm build # 0 type errors
pnpm lint # 0 lint errors
pnpm test # 0 failures
```

Run the specific test file first for fast iteration:

```bash
node --experimental-transform-types --test packages/bridge/test/<new-file>.test.ts
```

## Migration checklist template

For each legacy test file:

- [ ] Read and categorise all tests
- [ ] Delete parser-only and roundtrip tests (covered by regressionTest)
- [ ] Design bridges using test.multitool
- [ ] Write scenarios with correct assertions
- [ ] Ensure traversal coverage (add empty-array, fallback scenarios)
- [ ] Add `allowDowngrade: true` where compiler downgrades
- [ ] Handle GraphQL replay bugs with `assertGraphql: () => {}`
- [ ] Move non-runtime tests to corresponding package
- [ ] Keep tests needing custom execution (AbortSignal) using `forEachEngine`
- [ ] Verify: `pnpm build && pnpm lint && pnpm test`
- [ ] Don't delete the legacy file until confirmation

## Files remaining to migrate

```
packages/bridge/test/legacy/ # check for remaining legacy tests
packages/bridge/test/expressions.test.ts # if still using forEachEngine
packages/bridge/test/infinite-loop-protection.test.ts # if still using forEachEngine
```
4 changes: 2 additions & 2 deletions examples/builtin-tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"private": true,
"type": "module",
"scripts": {
"start": "node --experimental-transform-types --conditions source server.ts",
"e2e": "node --experimental-transform-types --conditions source --test e2e.test.ts"
"start": "node --experimental-transform-types server.ts",
"e2e": "node --experimental-transform-types --test e2e.test.ts"
},
"dependencies": {
"@stackables/bridge": "workspace:*",
Expand Down
4 changes: 2 additions & 2 deletions examples/composed-gateway/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"private": true,
"type": "module",
"scripts": {
"start": "node --experimental-transform-types --conditions source server.ts",
"e2e": "node --experimental-transform-types --conditions source --test e2e.test.ts"
"start": "node --experimental-transform-types server.ts",
"e2e": "node --experimental-transform-types --test e2e.test.ts"
},
"dependencies": {
"@stackables/bridge": "workspace:*",
Expand Down
4 changes: 2 additions & 2 deletions examples/travel-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"private": true,
"type": "module",
"scripts": {
"start": "node --experimental-transform-types --conditions source server.ts",
"e2e": "node --experimental-transform-types --conditions source --test e2e.test.ts"
"start": "node --experimental-transform-types server.ts",
"e2e": "node --experimental-transform-types --test e2e.test.ts"
},
"dependencies": {
"@stackables/bridge": "workspace:*",
Expand Down
4 changes: 2 additions & 2 deletions examples/weather-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"private": true,
"type": "module",
"scripts": {
"start": "node --experimental-transform-types --conditions source server.ts",
"e2e": "node --experimental-transform-types --conditions source --test e2e.test.ts"
"start": "node --experimental-transform-types server.ts",
"e2e": "node --experimental-transform-types --test e2e.test.ts"
},
"dependencies": {
"@stackables/bridge": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion examples/without-graphql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"scripts": {
"weather": "node --experimental-transform-types cli.ts weather.bridge '{\"city\":\"Berlin\"}'",
"sbb": "node --experimental-transform-types cli.ts sbb.bridge '{\"from\":\"Bern\",\"to\":\"Zürich\"}'",
"e2e": "node --experimental-transform-types --conditions source --test e2e.test.ts"
"e2e": "node --experimental-transform-types --test e2e.test.ts"
},
"dependencies": {
"@stackables/bridge": "workspace:*"
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@
"profile:heap": "node scripts/profile-heap.mjs",
"profile:deopt": "node scripts/profile-deopt.mjs",
"profile:flamegraph": "node scripts/flamegraph.mjs",
"bench:compare": "node scripts/bench-compare.mjs"
"bench:compare": "node scripts/bench-compare.mjs",
"mutants": "npx stryker run"
},
"devDependencies": {
"@changesets/changelog-github": "^0.6.0",
"@changesets/cli": "^2.30.0",
"@eslint/js": "^10.0.1",
"@stryker-mutator/core": "^9.6.0",
"@stryker-mutator/typescript-checker": "^9.6.0",
"@tsconfig/node24": "^24.0.4",
"eslint": "^10.0.2",
"tinybench": "^6.0.0",
Expand Down
Loading
Loading