Skip to content

catalog-tests: the catalog test harness, and flowctl raw test - #3417

Open
jgraettinger wants to merge 5 commits into
johnny/v2-tests-2-runtime-localfrom
johnny/v2-tests-3-catalog-tests
Open

catalog-tests: the catalog test harness, and flowctl raw test#3417
jgraettinger wants to merge 5 commits into
johnny/v2-tests-2-runtime-localfrom
johnny/v2-tests-3-catalog-tests

Conversation

@jgraettinger

@jgraettinger jgraettinger commented Aug 27, 2026

Copy link
Copy Markdown
Member

Description:

Stack 3 of 5, and the core of it: a new catalog-tests crate holding what a catalog test means, layered above runtime-local (#3416) and runtime-next. Plus the one protocol addition it needs, and the CLI that drives it.

Five commits:

  • proto: add derive Reset / ResetDone messages — the mechanism the harness uses to isolate test cases from one another.
  • runtime-next: forward Reset to derive connectors — handled shard-locally; the leader is not involved.
  • derive-sqlite: report no checkpoint for an ephemeral database — a :memory: database is not remote-authoritative, and saying so is what lets the harness run derive-sqlite multi-shard.
  • catalog-tests: the catalog test harness — the crate.
  • flowctl: add the raw test subcommand — and CI's ci:catalog-test task now uses it instead of flowctl-go test.

Workflow steps:

flowctl raw test --source examples/flow.yaml

A line per test, keyed by test name. A failure names the source path and JSON pointer of the step it failed at and indents the rendered diff beneath; a case the run never reached says so. Then a tally. Exits non-zero if any test failed.

Documentation links affected:

None user-facing. crates/catalog-tests/README.md is new and carries the design.

Notes for reviewers:

Suggested reading order: crates/catalog-tests/README.md — especially "Non-obvious details", which covers the two clock flavors that must never mix, journals keyed by partition_template.name because catalog names may nest, commit signalling through the Publisher seam, ordering guaranteed across store transactions but deliberately not within one, and redaction under an empty salt. Then run.rs and scheduler.rs, then the two test files.

clock / graph / scheduler follow go/testing/ closely, down to its unit tests, rather than re-deriving scheduling semantics that were already worked out. Two deliberate departures: the graph tracks derivations only, since a test never runs a capture or a materialization; and there is no ErrAdvanceDisabled, because synthetic time is a counter local to this crate rather than something a data plane must serve.

On raw test's placement. It sits under raw beside raw preview-next, which drives the same stack: both are local, developer-facing, and tied to a runtime that is still landing. flowctl catalog test — a remote dry-run publish — is a different command and is unaffected.

Tests run over derive-sqlite and need no containers, covering scheduling, Reset isolation, partition selectors, redaction, nested collection names, read delays, diff rendering, and a run dropped mid-flight. Connector death at startup is covered by tests/fixtures/dying_connector.py, a local: connector that dies on command, since no well-behaved connector produces that fault.


Stack — each PR is based on the one above it, so review only the top commits of each.

  1. Housekeeping and prep for the catalog-test harness #3415 — Housekeeping and prep (merged)
  2. runtime-local: extract the local task-drive layer from flowctl #3416 — runtime-local: extract the local task-drive layer
  3. catalog-tests: the catalog test harness, and flowctl raw test #3417 — catalog-tests: the harness, and flowctl raw testyou are here
  4. control-plane-api: run publication tests on catalog-tests #3418 — control-plane-api: run publication tests on catalog-tests
  5. Remove the V1 catalog-test machinery #3419 — Remove the V1 catalog-test machinery

Reset asks a derivation shard's connector to reset its internal state to an
as-just-initialized condition, mapping onto the connector protocol's existing
`derive.Request.Reset`. It is the mechanism the catalog-test harness uses to
isolate test cases from one another.

Reset is deliberately shard-local — Controller → Shard, with ResetDone coming
back Shard → Controller. The leader is not in the path: coordinating a common
transaction boundary across shards belongs to the caller, and the test harness
already has that property because it drives one transaction per stat() and
awaits its commit.

ResetDone is nonetheless required. A shard receives transaction messages from
its leader but Reset from its controller, so ordering within either stream
alone cannot establish that the connector observed the reset before the next
transaction's reads.
A derivation shard now answers a controller Reset by queueing a
`derive::Request.Reset` to its connector and replying ResetDone.

This is handled shard-locally; the leader is not involved.
`Opened.runtime_checkpoint` is how a connector tells the runtime that its
checkpoint state is remote-authoritative — that the endpoint, not the recovery
log, holds the truth. derive-sqlite reported one unconditionally, including for
a `:memory:` database whose "truth" does not outlive the session.

Introduce `Database` to decide this once, where the URI is chosen: `Durable` for
a file-backed database, whose recorded checkpoint is authoritative and is
reported; `Ephemeral` for `:memory:`, which reports none. An explicit `:memory:`
from `sqlite_vfs_uri` is now recognized as ephemeral rather than taken at face
value as a file path.

This is a test-only concern in practice: a production task is always threaded a
recorded recovery-log VFS URI by the runtime, and `:memory:` arises only where a
local drive deliberately wants a stateless connector. What it buys is that such
a drive is no longer forced single-shard by an authority claim the database can't
back — which is what lets the catalog-test harness run derive-sqlite multi-shard.
New crate holding what a catalog test *means*, layered above runtime-local and
runtime-next. `run_tests` starts a resident session per enabled derivation, then
runs every case — sorted by first-step scope, so a run is diffable from one
invocation to the next — through the scheduler, with a Reset between cases.
Which derivations to start is settled synchronously first, so a malformed catalog
fails before any connector boots.

Every derivation runs multi-shard, whatever its connector. Sessions thread an
empty `Task.sqlite_vfs_uri`, so a derive-sqlite shard runs a session-scoped
`:memory:` database and claims no checkpoint authority it couldn't back — which
is what leaves the leader free to accept the topology.

clock / graph / scheduler carry the scheduling semantics that `go/testing/`
worked out — writes cascading through a multi-hop chain, self-cycles reaching a
fixed point, read delays against a lazily-advanced synthetic clock, verify steps
gated on a forward BFS over pending reads — and follow the Go closely, down to
its unit tests, rather than re-deriving them. Two deliberate departures: the
graph tracks derivations only, since a test never runs a capture or a
materialization; and there is no `ErrAdvanceDisabled`, because synthetic time is
a counter local to this crate.

Everything else that's non-obvious is written up under "Non-obvious details" in
the crate README.

Tests run over derive-sqlite and need no containers, covering scheduling, Reset
isolation, partition selectors, redaction, collection names nested under one
another, read delays, diff rendering, and a run dropped mid-flight. Connector
death at startup — while handling Open, or racing the leader's startup commit
just after Opened — is covered by `tests/fixtures/dying_connector.py`, since no
well-behaved connector produces that fault.
`flowctl raw test --source ...` runs a catalog's tests locally on the
runtime-next stack, and CI's catalog-test task now uses it instead of
`flowctl-go test`.

It sits under `raw` beside `raw preview-next`, which drives the same stack: both
are local, developer-facing, and tied to a runtime that is still landing, so
neither belongs at the top level yet. `flowctl catalog test` — a remote dry-run
publish — is a different command and is unaffected.

Output is a line per test, keyed by test name: a failure names the source path
and JSON pointer of the step it failed at and indents the rendered diff beneath,
and a case the run never reached says so. Then a tally. Exits non-zero if any
test failed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant