feat: implement specs/testing-spec.md — <Testing>, <Test>, assertions, xmd test#108
Conversation
core: - expandInvocation extension hook on the Component Api - extensions claim raw invocations before built-in expansion; default answers unhandled - ComponentInvocation carries SourcePosition (original-file offset, line, column - frontmatter included, body start computed as a verbatim suffix) - runDocument output becomes a replay-safe stream (retained history, one queue per subscriber); error path closes with accumulated output @executablemd/testing (new workspace package): - Test Api: testing/inTest/verbose values, record/results/boundary ops - <Testing> boundaries: subtree activation, delegating collectors, per-boundary zero-test rule; nested boundaries delegate outward - <Test>: child Effection scope + test-owned EvalScope lease, stable isolated binding env, raise interception (ErrorSegments fail the test), 20s timebox, setup/body/teardown phase classification, text-only returns - 14 @std/assert assertion components: live expression evaluation over projected+current env, expected-children (Capture parity) XOR expected prop, assert-then-format diagnostics with guarded fallback - executeDocument: single scoped wrapper for run and test modes; lazy output stream, run-level collectors, TestFailureError after the report cli: - xmd test subcommand; run and test share one driver over executeDocument - exit 1 on test failure or document abort; --verbose renders assertion diagnostics during regular execution release: publish workflow regenerated, lockstep spec updated, testing in all workspace manifests and lockfiles; @jsr scope registry for npm/bun
PR #108: feat: implement specs/testing-spec.md — , , assertions, xmd test87 files, +4477 / -1130 Scope🔴 PR has 5607 lines changed. Split into focused PRs. 🟡 5607 lines changed. PRs under 400 receive more thorough review. 🟡 87 files changed. Are all changes related? 🟡 PR mixes config and source changes. 🟡 New abstraction files: testing/tests/helpers.ts. Verify 3+ consumers. Structural🟡 2 console statements. Slop
Static Analysis✅ Oxlint found no issues. CorrectnessNo extraneous code patterns detected. |
|
Re the 12 'redundant comment' suggestions: 10 of the flagged lines are |
- <Test> isolated environment now merges caller-projected bindings under the current environment (core's precedence), so tests projected through <Content /> keep caller eval bindings; regression covers an eval block inside a projected test reading a caller binding - msg is validated as a string by type check alone, never formatted before the assertion runs; regression proves hostile toJSON/toString on a non-string msg cannot replace the outcome - replace prohibited 'as' assertions with a declared union array, filtered env construction, and Reflect.get/deleteProperty - drop two spec-restating comments; lint suppressions and lifecycle/ precedence comments retained
core: - rename runDocument to execute (no alias); ExecuteOptions; files renamed - deliver execute through a new test-agnostic Execution context Api - extensions decorate the execution lifecycle with Execution.around middleware instead of wrapping execution functions - DocumentExecution completes with Result<string>: Ok(output) or Err(error); once a handle exists completion never throws - the whole spawned setup and workflow sit in one error boundary that closes output with complete-or-partial text and resolves Err - collect unwraps the Result (string on Ok, throw on Err) @executablemd/testing: - executeDocument removed; useTesting() is scope-local composition: vocabulary + collection + completion-policy middleware + root activation, returning a session whose results operation snapshots discovery order; one session per execution scope, enforced; middleware dies with the scope - vocabulary registration installs an Execution policy so explicit <Testing> boundaries turn failures or empty boundaries into Err even when root testing is inactive - an existing core Err passes through unchanged; results stay available after failure cli: - xmd run: vocabulary registered, root testing inactive - xmd test: useTesting() composed around the same core execute call - both inspect the completion Result for exit status specs: executable-mdx-spec (execute, Execution Api, Result semantics), testing-spec (useTesting composition and completion semantics)
- useTesting() session supports exactly one execute() call - a second call fails clearly before a handle exists (results are cumulative, so a zero-test second document would otherwise inherit the first document's passing outcome); regression added - OA10 decision row updated to Result/Err completion semantics - stale runDocument / run-document.ts / executeDocument references replaced in README, runtime docs and stubs, decisions log, test-api - 'a ExecuteOptions' -> 'an ExecuteOptions' - revert formatting-only churn in core/components/*.md
Structure source through names and modules. Enforcement via a local oxlint rule with autofix is tracked in #109.
durableRun returns the stored root result on a full replay without re-expanding the document, so test and boundary collectors stayed empty: a passing session replayed as 'no tests were discovered' and a failing <Testing> boundary replayed as success. - journal.ts: each completed TestResult and each <Testing> boundary outcome persists as a testing-owned durable operation (test_result, testing_boundary) during expansion, before the root Close, identified deterministically by source position; payloads validated with type guards on read - vocabulary Execution middleware: on a confirmed full replay (root Close present in ExecuteOptions.stream) the stored records are restored into the current collectors through the same record/boundary operations live expansion uses; live and partial journals hydrate nothing - re-expansion records each result exactly once with completed records replaying in place, and stored results stay authoritative over recomputation - replay regressions: passing/failing/zero-test useTesting live+replay, pass/fail/empty explicit <Testing> boundaries, partial replay without duplicates, results after replayed failure, unchanged output and zero re-run effects (appendCount 0) - testing spec documents the durable record and replay semantics - @executablemd/durable-streams added as a direct testing dependency
…e-streams dependency
… API - smoke-test/README.md captures the entire guide once into a root binding and re-emits it, so regular xmd run output stays semantically unchanged and every document effect runs exactly once; seven named, top-level, sibling <Test> bodies (Components, Execution, Captures, Evaluation, Providers, Output regions, Durability) inspect the captured guide with AssertStringIncludes / AssertMatch / AssertNotMatch - no <Testing> wrapper, so regular execution skips them - harness moves to testing/tests/smoke.test.ts (core stays independent of @executablemd/testing) and shrinks to infrastructure: useTesting() composed around core execute(), the exact seven embedded tests asserted in discovery order all passing, journal size checked host-side, and a fresh-session full replay asserted equal in Result and TestResult snapshot with zero appended journal events
The smoke guide splits into self-testing feature documents under
smoke-test/Guide/. Each document captures its own rendered content into a
local binding, re-emits it, and carries a sibling named <Test> asserting
against that capture - no <Testing> wrapper, no nesting, no re-executed
effects. README.md composes the sixteen feature documents in guide order
and keeps the root-frontmatter behavior and its test, since that behavior
belongs to the root document.
Expression Props and Text Interpolation share the itemCount binding, so
they stay together in Guide/Interpolation.md (with its own title meta so
the {meta.title} demo renders identically).
The harness now expects the exact seventeen embedded test names in
discovery order; execution, journal, result, and replay checks remain the
only TypeScript assertions.
…n sites Every embedded test now connects a specific captured result to its exact expected value instead of searching the whole rendered section: - demos are captured at their production site and re-emitted immediately; assertions use AssertEquals/AssertStrictEquals for deterministic values, anchored AssertMatch for genuinely dynamic ones (file count, timestamp, allocated port), and AssertNotMatch only for deliberately suppressed content - README asserts the exact rendered heading, version statement, and repo link; Components adds a Content-slot sentinel Section (hoisted to doc level - Section cannot nest inside Section); Execution isolates file count, visible exec, empty silent output, and yaml passthrough; Interpolation splits into Expression props and Text interpolation tests asserting exact interpolated lines; Captures asserts the four bindings directly plus equality-based site captures proving captured-only content never rendered; Healing uses a genuinely unclosed ** marker and asserts the exact healed boundary; Evaluation asserts live bindings (message, serverReady, startedAt, port) and the port-derived exec line, with the eval-block region asserted to render prose only; Daemons equals the curl response to daemon-ok; Sampling proves which content reached the provider (StubProvider now echoes context.content); Instructions asserts the exact system prompt; NamedSlots asserts complete table shapes; OutputRegions isolates <OutputDemo /> output - the nested-Instruction accumulation demo is impossible (engine cycle detection rejects Instruction within Instruction), so the composition claim is removed per the review's fallback - Overview and Summary tests removed (prose-only / tautological) - harness updated to the sixteen exact test names in discovery order and stripped of its explanatory header
Every embedded test now owns its full scenario - setup, action, capture, and assertion live inside the same <Test>, connected directly to the captured actual value: - guide prose stays outside tests; test-owned demos moved inside, so regular execution renders the prose guide while xmd test runs every scenario (test examples are intentionally skipped during regular runs) - the daemon test contains port allocation, daemon startup, readiness polling, and the curl assertion, so teardown is bounded by the atomic test scope - feature-group tests split into 36 individually named atomic tests, one behavioral outcome each; no test depends on bindings or effects from a sibling (each defines its own eval bindings and providers) - capture re-emissions removed - assertion diagnostics carry the actual and expected values - harness updated to the 36 exact names in discovery order
New distinct 'smoke' CI job: pinned Deno, deno task build, then the ./dist/xmd binary runs 'test smoke-test/README.md' directly with --raw, failing on the binary's exit code. The Deno suite keeps the focused API replay tests; the compiled binary is the authoritative end-to-end check. The binary initially failed every eval block using when()/fetch(): STANDARD_IMPORTS resolve at runtime from generated eval modules, so 'deno compile --exclude-unused-npm' pruned @effectionx/converge and @effectionx/fetch from the binary. The compiler modules that inject STANDARD_IMPORTS now statically anchor those packages, keeping them in any compile graph that embeds core.
|
Triage of the b3cf9e3/8ce9430 review passes:
|
Motivation
Executable documents can contain probabilistic behavior (LLM sampling, subprocesses, network). specs/testing-spec.md (added in this PR) grounds that behavior in observable results:
<Testing>/<Test>directives, fourteen@std/assert-backed assertion components, aTestApi(testingoperation), and anxmd testCLI command. None of it existed before this PR.Approach
Testing is not an engine feature — it is a vocabulary extension composed around a single core execution entrypoint.
Core owns the only entrypoint, and testing composes middleware around it:
executeentrypoint, delivered through a new test-agnosticExecutioncontext Api. Extensions decorate the execution lifecycle withExecution.around({ execute })middleware — observing options, wrapping the handle, or mapping its completion — without introducing another execution function. Core has no testing flag and imports no testing concepts.DocumentExecutioncompletes withResult<string>:Ok(output)on success,Err(error)on document, infrastructure, or testing failure. Onceexecutehas returned a handle, completion never throws — the entire spawned setup and workflow sit in one error boundary that closes the output stream (with complete or partial rendered text) and resolvesErr. Pre-handle failures may still throw.collectunwraps the Result for callers that want throw-on-failure ergonomics.useTesting()installs scope-local middleware and returns the results handle: the expansion vocabulary (via core'sexpandInvocationhook), result collection, root testing activation (Test.around({ testing: () => true })— the same install<Testing>performs for a subtree, makingxmd test≡ root<Testing>), and a completion policy that converts an otherwise successful execution toErr(TestFailureError)after output closes when tests failed or none were discovered. A coreErrpasses through unchanged andresults(an immutable discovery-order snapshot) stays available after failure. One session per execution scope and oneexecute()per session, both enforced; every install is removed with the session's Effection scope.installTestingVocabularyalone (used byxmd run) keeps testing mode off —<Test>bodies are skipped entirely, assertions stay usable (diagnostics with--verbose), and an explicit<Testing>boundary still activates its subtree and turns boundary failures or an empty boundary into anErroutcome.<Test>isolation: child Effection scope, a test-owned EvalScope lease (a dedicated suspended task inside the parent EvalScope, halted during test teardown so persisted middleware cannot leak into later tests), one stable binding environment merging caller-projected bindings under the current environment, araiseinterceptor installed as outer instrumentation so every raised ErrorSegment (command failures, import failures, nested<Test>,<Output>-region errors) fails the current test while later tests run, a directly-yielded 20stimebox(DI seamcreateTestHandlers({ timeoutMs })for fast tests), and setup/body/teardown failure classification. A completed test returns only text segments — containment survives throwing ambient raise policies.RegExp/undefined/object identity survive;msgis type-checked as a string, never formatted); diagnostics are built only after the outcome is fixed, under guarded fallback.TestResultand every explicit<Testing>boundary outcome is journaled as a testing-owned durable operation (test_result,testing_boundary) during expansion, before the root Close — identified deterministically by source position. On a confirmed full replay (the journal already holds a root Close, so the document is not re-expanded and no effects rerun) the stored records are restored fromExecuteOptions.streaminto the current collectors before the completion policy applies, so passing, failing, and zero-test outcomes — root sessions and explicit boundaries alike — replay exactly as they ran live. Live and partial journals hydrate nothing; re-expansion records each result exactly once, in discovery order, with completed records replaying in place. Payloads are validated with type guards on read.ComponentInvocationcarries deterministic original-file source positions (frontmatter included; body start computed as a verbatim suffix, never content search), and execution output is a replay-safe stream (retained history + one queue per subscriber) so late and repeated subscribers receive every chunk and the close value.smoke-test/Guide/. Guide prose stays outside tests; each of the 36 atomic<Test>elements owns its complete scenario — setup, action, capture, and assertion inside the same test, connected directly to the captured actual value (AssertEquals/AssertStrictEqualsfor deterministic outputs and live bindings, anchoredAssertMatchfor genuinely dynamic values,AssertNotMatchonly for deliberately suppressed content). One behavioral outcome per test; no test depends on a sibling's bindings or effects; lifecycle-dependent scenarios like the daemon test contain startup, readiness, and assertion so teardown is bounded by the atomic test scope. Regularxmd runrenders the prose guide (test examples are intentionally skipped);xmd testruns every scenario.testing/tests/smoke.test.tsis infrastructure only:useTesting()around coreexecute(), the 36 exact test names asserted in discovery order, journal size host-side, and a fresh-session full replay asserted identical with zero appended events. (The nested-Instruction accumulation demo is omitted: the engine's cycle detection rejectsInstructionwithinInstruction, so composition cannot be demonstrated in-document.)xmd runregisters the vocabulary aroundexecute;xmd testcomposesuseTesting()around the sameexecutecall. Both inspect the returnedResultfor exit status (0 onOk, 1 on anyErr).Scope confirmation
New abstractions (if any)
Execution/ExecutionApi(core execution middleware hook),InvocationContext/InvocationHandling(core expansion hook contract),TestApi/TestResult/BoundaryOutcome/Testing(spec-mandated),ReplayStream(fixes chunk loss for any consumer),SourcePosition(spec-mandated test identity).New dependencies (if any)
@std/assert(jsr, already resolved in deno.lock; npm alias@jsr/std__assertvia the@jsrscope registry added to .npmrc/bunfig.toml)@std/assertsemantics and error types.New tests (if any)
core/tests/expand-invocation.test.ts(6): fall-through, claim, handled-empty vs unhandled, raise policy, context passingcore/tests/source-position.test.ts(6): local/origin translation, frontmatter offset with a body-line copy inside frontmatter (defeats content-search false matches), imported componentscore/tests/replay-stream.test.ts(6): replay-before-subscribe, replay/live boundary exactly-once, multiple subscribers, subscribe-after-close, post-close ignoretesting/tests/assertions.test.ts(17): visibility rules in/out of testing mode and with--verbose, abort semantics outside tests, expected-children Capture parity + XOR validation, per-kind prop validation, RegExp requirement, live/projected bindings, string-onlymsgtype check, assert-then-format regressionstesting/tests/testing-mode.test.ts(19): skip-with-zero-side-effects, explicit<Testing>under regular runs, root activation, zero-test failure, nested boundary delegation, ErrorSegment failures (exec/import/nested-test/<Output>-region), containment in throwing-policy regions, projected<Test>bodies reading caller bindings, binding + effect isolation across tests (lease teardown), partial-output-before-failure ordering, timeout halt + continue, source-location identitytesting/tests/use-testing.test.ts(10): session composition + results snapshot, results after failure, coreErrpreserved, oneexecute()per session, one session per scope, middleware removed with the scope,Err-not-throw completion, pre-handle throw path, late subscriber integrity, early caller-scope halt teardowntesting/tests/replay.test.ts(6): passing/failing/zero-testuseTesting()live + full replay, pass/fail/empty explicit<Testing>boundaries on replay, partial replay without duplicated results, results available after replayed failure, unchanged output with zero re-run effectstesting/tests/cli.test.ts(4):xmd testexit 0/1 with piped report, zero-test exit 1,xmd runskip + exit 0testing/tests/smoke.test.ts(1): the smoke guide's 36 embedded atomic tests pass in discovery order, live and on full replay, with identical Result/TestResult snapshots and no appended journal eventsRelease notes
@executablemd/testingstarts at the lockstep version 0.3.1. The publish workflow was regenerated (deno task gen:publish-workflow); testing publishes after core. Manual npm/JSR bootstrap (release-process-spec §6 steps 2–4) is a maintainer step outside this PR: firstnpm publishby hand, trusted publisher config, and JSR package creation. Follow-up tracked separately: thelocal/no-section-divider-commentsoxlint rule (#109).