Repository navigation
test(machines,tooling): check every engine with one conformance runner - #95
Merged
Merged
Conversation
…re missing The fixture locator returned null when Trax.Api.StateMachine/machines was not found, and every cross-engine test then called Assert.Ignore, a leftover from when the TypeScript engine was a separate repository. In this repository the folder is always there, so a copy or a CI path filter that left it out reported green without comparing the engines. The locator now throws, naming every directory it searched.
The canonical-wire vectors were copied into a C# and a TypeScript test and had drifted: only C# checked lone surrogates and the \b \f \r escapes. The format verdicts lived in a TS-only table beside the C# strict-input cases. Both are now files under Trax.Api.StateMachine/conformance/ that each engine reads: - canonical-wire.json, the union of both tables. - strict-input/, the strict machine's IR and every StrictInputMachine case with the C# engine's answer, written by StrictInputGoldenTests (UPDATE_CONFORMANCE=1) and replayed by vitest without .NET. The seven TS-only format rows moved into the C# cases, where the CLR decides their verdict. - refusal-codes.json, the closed set both engines check themselves against, in place of C# parsing results.ts with a regex.
The IR was defined by the exporter and the TypeScript interpreter, and described in prose on the IR format page. An engine in another language could only be written by reading the TS code. conformance/ir.schema.json (draft 2020-12) now states it, with every object closed so an unknown field is refused rather than ignored. vitest validates every committed IR against it and checks that it refuses an extra field, a misspelt rule member, an unknown rule kind and an output on a non-done outcome. The C# IrSchemaTests exports every declarative test machine and validates it through the same validator (tools/conformance/ir-schema.mjs, over node, ajv as a dev dependency), so a field the exporter starts writing fails until the schema names it. The IR format page says the schema is normative.
Each engine read the shared fixtures its own way, inside its own test framework, so a third engine would need a reader for every fixture shape as well as an engine. tools/conformance/run.mjs turns every case in machines/ and conformance/ into requests and sends them to one engine, the testee, over a line-delimited JSON protocol written down in conformance/PROTOCOL.md (load, advance, outcome, timer, rehydrate, serialize, canonical). It compares only codes and wire bytes. The TypeScript testee builds each machine from its IR through the package's exports. The C# testee serves its test machines by name and answers from inside NUnit, with the runner started in --stdio mode, so no separate executable is needed. Its load step checks that the committed IR is the one the C# machine exports, apart from the test-only differential block. Both pass all 417 cases, and a testee that gives one case the wrong code fails with that case named. The strict-input golden now records the wire after a transition, as the protocol answers it.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
… the timeline follows A_running_runs_steps_are_pushed_to_its_timeline_as_they_end released the held run as soon as the page sent its OnJunctionEvent subscribe frame. graphql-ws acknowledges no single subscription, so the server could still be registering it when the junction completed, and that event reached nobody: the test then timed out waiting for the push (three CI runs on 2026-10-20, on branches that touch neither the console nor subscriptions). The test now publishes a probe junction event for the run through the host's junction event handlers until the page receives it, as the Api's JunctionEventsE2ETests already does with WaitUntilListeningAsync, and releases the run only then.
Contributor
|
This PR is included in version 1.68.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Every state-machine engine is checked by one conformance kit and one runner
Background
A Trax machine is declared in C# and exported as an IR: one canonical JSON document holding its states,
triggers, context and input schemas, and each transition's guard and reducer as data. Two engines run it. The C#
engine (
Trax.Effect.StateMachine) is the source of truth and runs on the server. The TypeScript engine(
@traxsharp/state-machine, the twin) interprets the IR in the browser and predicts what the server willanswer. They must agree byte for byte on the canonical wire (a snapshot's JSON in RFC 8785 form: keys sorted
by UTF-16 code unit, numbers and strings exactly as
JSON.stringifywrites them) and code for code on arefusal (
GUARD_FAILED,INVALID_INPUT, ...). Messages may differ.Until now they agreed because both test suites read shared fixtures under
Trax.Api.StateMachine/machines/, eachin its own way. The plan is to add engines and bindings in other languages, so the agreement has to be something
a third engine can join without reading either test suite.
Problem
Example
The canonical-wire vectors are now one file both engines read. A string holding a lone high surrogate cannot pass
through System.Text.Json, so the file gives it as UTF-16 code units:
{"name":"string: a lone high surrogate is escaped","stringUnits":[55296],"wire":"{\"s\":\"\\ud800\"}"}Before, only the C# test had this vector. Now the TS engine checks it too (it already escaped it correctly).
The runner turns that vector, and every other case in the kit, into one request line for the engine under test,
the testee. It waits for one answer line back. A turnstile case from the differential corpus:
If the testee answered
INVALID_INPUTinstead, the run fails and names the case:The C# engine cannot build a machine from an IR, so its testee serves its test machines by name. On
loaditchecks that the committed IR is the one its machine exports. On the first run this found that the committed
checkout.ir.jsoncarries adifferentialblock (test-only fuzzing samples) that the C# checkout does notdeclare. That block is stripped from a runtime machine and is not behaviour, so the check ignores it.
How this solves it
Both engines pass all 417 cases. The TS run is checked both in process and as a spawned testee, and a test flips
one answer to check that the runner fails and names the case.
Architectural hinges
Trax.Api.StateMachine/conformance/PROTOCOL.md: the contract a new engine implements. Every op, whatthe runner sends from each source, and what it leaves to each engine (the v1→v2 migration golden, whose v2
machine has no IR).
Trax.Api.StateMachine/tools/conformance/kit.mjs(kitCases,wireOf): turns every fixture shape intoa request and its expected answer. It is engine-independent, so the expectations cannot come from the engine
under test.
Trax.Api.StateMachine/tools/conformance/run.mjs(runCases): the comparison (canonical JSON of theanswer against the expectation) and the two transports.
Trax.Effect/tests/Trax.Effect.StateMachine.Tests/Helpers/ConformanceTestee.cs: the C# side of theprotocol, and the load check that ties each committed IR to its C# source.
Trax.Api.StateMachine/conformance/ir.schema.json: the IR's normative shape, which the exporter's outputand every committed IR are validated against.
Also on this branch: the console timeline test waits for the server