Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
200b2b8
conformance(ts): persist operation field on wire snapshots
jeremy May 4, 2026
277c335
conformance(ruby): wire-replay runner + schema walker
jeremy May 4, 2026
4f0a970
conformance(python): wire-replay runner + schema walker
jeremy May 4, 2026
62fc17a
conformance(go): wire-replay runner + schema walker
jeremy May 4, 2026
6cb4bf3
conformance(kotlin): wire-replay runner + schema walker
jeremy May 4, 2026
8fa5347
conformance: wire-replay make targets + CONTRIBUTING + gitignore
jeremy May 4, 2026
af427c8
conformance(ruby): align walker required-paths with `/`; fix mode-gat…
jeremy May 8, 2026
6be60d7
conformance(go): align walker required-paths with `/`
jeremy May 8, 2026
750759a
conformance(kotlin): tighten env-var gate + narrow Throwable catches
jeremy May 8, 2026
94d2247
conformance(go,ruby): fix walker docstring drift on required-path sep…
jeremy May 12, 2026
a666171
conformance(python,go): preserve empty bodyText on wire snapshots
jeremy May 13, 2026
e17d393
conformance: align default-response precedence with documented order
jeremy May 13, 2026
00eda7f
conformance(kotlin): drop unused JsonArray and encodeToJsonElement im…
jeremy May 13, 2026
9ef1357
conformance: faithful empty-body decoding + Kotlin gate hardening
jeremy May 13, 2026
b4574c0
gitignore: stray Go binary from `go build` in conformance runner
jeremy May 13, 2026
39d977e
conformance: surface snapshot read/parse errors as gate messages
jeremy May 13, 2026
6a916ed
conformance(python): catch UnicodeDecodeError on snapshot read
jeremy May 13, 2026
defab06
conformance: address four Copilot threads on PR #301
jeremy May 14, 2026
9550689
conformance(ts): keep persisted operation authoritative in snapshot s…
jeremy May 14, 2026
ee7bff9
conformance: gate on non-empty pages + matching pages_count in replay…
jeremy Jul 22, 2026
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ spec/build/

# Conformance test runner artifacts
conformance/runner/go/conformance-runner
conformance/runner/go/go
conformance/runner/ruby/Gemfile.lock
conformance/runner/typescript/node_modules/

Expand Down Expand Up @@ -37,7 +38,11 @@ python/dist/
python/.pytest_cache/
python/.coverage
python/src/*.egg-info/

# Python conformance runner artifacts
conformance/runner/python/.venv/
conformance/runner/python/**/__pycache__/
conformance/runner/python/**/*.pyc
conformance/runner/python/uv.lock

# TypeScript SDK build artifacts
Expand Down
71 changes: 70 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,76 @@ gate refuses to run if any fixture operation lacks a dispatch.
Because live canary fixtures live in the shared `conformance/tests/` directory,
offline conformance runners must treat `mode` as part of the shared schema and
execute only mock tests: omitted `mode` or `mode: "mock"`. `mode: "live"` entries
belong to the TypeScript live wire-capture runner until replay runners are added.
belong to the TypeScript live wire-capture runner and the cross-language
wire-replay runners described next.

### Wire replay (cross-language)

The TypeScript live runner is the single canonical wire-capturer. When invoked
with `LIVE_RECORD_DIR=<path>`, it persists every captured response to
`<path>/<backend>/wire/<test>.json` with the snapshot format
`{ operation, pages: [{status, headers, body, bodyText, url}], pages_count }`.

The Ruby, Python, Go, and Kotlin runners each have a *wire-replay mode* that
reads those snapshots and decodes each page through their SDK. No HTTP calls,
no mock servers — the input is the canonical wire bytes captured by the TS
runner. Decode results land at
`<path>/<backend>/decode/<language>/<test>.json` with the format
`{ schema_version, operation, pages: [{decoded, decode_error, missing_required, extras_seen}] }`.

Each runner enforces three coverage gates at startup before doing any decode
work:

1. **Decoder coverage** — every operation in `live-my-surface.json` has a
decode case in this runner.
2. **Snapshot completeness** — every operation in `live-my-surface.json` has
a corresponding snapshot file at `<path>/<backend>/wire/`.
3. **Snapshot recognition** — every snapshot's `operation` field is in
`live-my-surface.json` (catches drift between TS dispatch and the shared
fixture).

Each gate prints which operations triggered it so the operator can fix the
right side: TS dispatch, the fixture, or the replay runner.

Two-stage flow:

```bash
# Step 1: TS captures canonical wire snapshots (live HTTP, requires creds).
BASECAMP_LIVE=1 \
BASECAMP_TOKEN=<token> \
BASECAMP_ACCOUNT_ID=<account> \
BASECAMP_BACKEND=bc4 \
LIVE_RECORD_DIR=tmp/canary \
make conformance-typescript-live

# Step 2: each language replays those snapshots through its SDK (offline).
for lang in ruby python go kotlin; do
WIRE_REPLAY_DIR=tmp/canary BASECAMP_BACKEND=bc4 \
make conformance-$lang-replay
done
```

Step 2 needs no credentials and no network — it's pure decode + walk. The
extras-observed output across languages is a consistency check on the
hand-rolled schema walkers (which mirror the TS validator's required + extras
algorithm in each language). Any per-language divergence in `extras_seen`
points at a walker bug in the diverging language.

`make conformance-typescript-live` is owned by PR 2; the four
`make conformance-*-replay` targets ship in PR 3. The orchestrating
`make conformance-live` and `make check-bc5-compat` targets that thread
TS capture → replay → BC4↔BC5 comparison together land in PR 4.

When the SDK gains a new operation in `live-my-surface.json`, it must be
added to:

- `conformance/runner/typescript/live-dispatch.ts` — TS dispatch case.
- `conformance/runner/ruby/replay-runner.rb` — Ruby decoder.
- `conformance/runner/python/replay_runner.py` — Python decoder.
- `conformance/runner/go/replay_runner.go` — Go decoder.
- `kotlin/conformance/src/main/kotlin/com/basecamp/sdk/conformance/ReplayRunner.kt` — Kotlin decoder.

Each runner's coverage gate refuses to start until all five are in place.

## API gap registry (`spec/api-gaps/`)

Expand Down
55 changes: 46 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ py-clean:
# Conformance Test targets
#------------------------------------------------------------------------------

.PHONY: conformance conformance-go conformance-kotlin conformance-typescript conformance-typescript-live conformance-ruby conformance-python conformance-build oauth-fixtures-check
.PHONY: conformance conformance-go conformance-go-replay conformance-kotlin conformance-kotlin-replay conformance-typescript conformance-typescript-live conformance-ruby conformance-ruby-replay conformance-python conformance-python-replay conformance-build oauth-fixtures-check

# Pinned validator for the data-only OAuth discovery fixtures. Run via uvx so the
# version is reproducible without a global install; the schema is separate from
Expand All @@ -398,11 +398,27 @@ conformance-go: conformance-build
@echo "==> Running Go conformance tests..."
cd conformance/runner/go && ./conformance-runner

# Run Go wire-replay against snapshots written by the TS live runner.
# Required env: WIRE_REPLAY_DIR, BASECAMP_BACKEND. Opt-in: not in `make check`.
conformance-go-replay:
@echo "==> Running Go wire-replay runner..."
@test -n "$$WIRE_REPLAY_DIR" || (echo "WIRE_REPLAY_DIR is required" >&2; exit 1)
@test -n "$$BASECAMP_BACKEND" || (echo "BASECAMP_BACKEND is required" >&2; exit 1)
cd conformance/runner/go && go run .

# Run Kotlin conformance tests
conformance-kotlin:
@echo "==> Running Kotlin conformance tests..."
cd kotlin && ./gradlew :conformance:run

# Run Kotlin wire-replay against snapshots written by the TS live runner.
# Required env: WIRE_REPLAY_DIR, BASECAMP_BACKEND. Opt-in: not in `make check`.
conformance-kotlin-replay:
@echo "==> Running Kotlin wire-replay runner..."
@test -n "$$WIRE_REPLAY_DIR" || (echo "WIRE_REPLAY_DIR is required" >&2; exit 1)
@test -n "$$BASECAMP_BACKEND" || (echo "BASECAMP_BACKEND is required" >&2; exit 1)
cd kotlin && ./gradlew --quiet :conformance:runReplay

# Run TypeScript conformance tests
conformance-typescript:
@echo "==> Running TypeScript conformance tests..."
Expand All @@ -424,11 +440,27 @@ conformance-ruby:
@echo "==> Running Ruby conformance tests..."
cd conformance/runner/ruby && bundle install --quiet && ruby runner.rb

# Run Ruby wire-replay against snapshots written by the TS live runner.
# Required env: WIRE_REPLAY_DIR, BASECAMP_BACKEND. Opt-in: not in `make check`.
conformance-ruby-replay:
@echo "==> Running Ruby wire-replay runner..."
@test -n "$$WIRE_REPLAY_DIR" || (echo "WIRE_REPLAY_DIR is required" >&2; exit 1)
@test -n "$$BASECAMP_BACKEND" || (echo "BASECAMP_BACKEND is required" >&2; exit 1)
cd conformance/runner/ruby && bundle install --quiet && ruby replay-runner.rb

# Run Python conformance tests
conformance-python:
@echo "==> Running Python conformance tests..."
cd conformance/runner/python && uv sync && uv run python runner.py

# Run Python wire-replay against snapshots written by the TS live runner.
# Required env: WIRE_REPLAY_DIR, BASECAMP_BACKEND. Opt-in: not in `make check`.
conformance-python-replay:
@echo "==> Running Python wire-replay runner..."
@test -n "$$WIRE_REPLAY_DIR" || (echo "WIRE_REPLAY_DIR is required" >&2; exit 1)
@test -n "$$BASECAMP_BACKEND" || (echo "BASECAMP_BACKEND is required" >&2; exit 1)
cd conformance/runner/python && uv sync && uv run python replay_runner.py

# Run all conformance tests
conformance: oauth-fixtures-check conformance-go conformance-kotlin conformance-typescript conformance-ruby conformance-python
@echo "==> Conformance tests passed"
Expand Down Expand Up @@ -694,14 +726,19 @@ help:
@echo " swift-clean Remove Swift build artifacts"
@echo ""
@echo "Conformance:"
@echo " conformance Run all conformance tests"
@echo " conformance-go Run Go conformance tests"
@echo " conformance-kotlin Run Kotlin conformance tests"
@echo " conformance-typescript Run TypeScript conformance tests"
@echo " conformance-ruby Run Ruby conformance tests"
@echo " conformance-python Run Python conformance tests"
@echo " conformance-build Build Go conformance test runner"
@echo " oauth-fixtures-check Validate OAuth discovery fixtures against their schema"
@echo " conformance Run all conformance tests"
@echo " conformance-go Run Go conformance tests"
@echo " conformance-go-replay Decode TS-captured wire snapshots through Go SDK"
@echo " conformance-kotlin Run Kotlin conformance tests"
@echo " conformance-kotlin-replay Decode TS-captured wire snapshots through Kotlin SDK"
@echo " conformance-typescript Run TypeScript conformance tests"
@echo " conformance-typescript-live Run TypeScript live canary against a real backend"
@echo " conformance-ruby Run Ruby conformance tests"
@echo " conformance-ruby-replay Decode TS-captured wire snapshots through Ruby SDK"
@echo " conformance-python Run Python conformance tests"
@echo " conformance-python-replay Decode TS-captured wire snapshots through Python SDK"
@echo " conformance-build Build Go conformance test runner"
@echo " oauth-fixtures-check Validate OAuth discovery fixtures against their schema"
@echo ""
@echo "Ruby SDK:"
@echo " rb-generate Generate types and metadata from OpenAPI"
Expand Down
23 changes: 23 additions & 0 deletions conformance/runner/go/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,29 @@ type TestResult struct {
}

func main() {
// Wire-replay mode gate: when WIRE_REPLAY_DIR is set, dispatch to
// the replay runner (replay_runner.go) and exit. The replay runner
// consumes wire snapshots written by the canonical TS live runner;
// see conformance/runner/typescript/live-runner.test.ts. Mock mode
// (the rest of this function) runs only when the gate is unset.
if dir := os.Getenv("WIRE_REPLAY_DIR"); dir != "" {
backend := os.Getenv("BASECAMP_BACKEND")
if backend == "" {
fmt.Fprintln(os.Stderr, "BASECAMP_BACKEND is required when WIRE_REPLAY_DIR is set")
os.Exit(1)
}
// Match the existing relative-path convention in this file: the
// runner is invoked with cwd = conformance/runner/go.
fixturePath := filepath.Join("..", "..", "tests", "live-my-surface.json")
openapiPath := filepath.Join("..", "..", "..", "openapi.json")
runner, err := NewReplayRunner(dir, backend, fixturePath, openapiPath)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
os.Exit(runner.Run())
}

testsDir := filepath.Join("..", "..", "tests")

files, err := filepath.Glob(filepath.Join(testsDir, "*.json"))
Expand Down
Loading
Loading