Skip to content

feat: implement specs/testing-spec.md — <Testing>, <Test>, assertions, xmd test#108

Merged
taras merged 14 commits into
mainfrom
feat/testing-spec
Jul 20, 2026
Merged

feat: implement specs/testing-spec.md — <Testing>, <Test>, assertions, xmd test#108
taras merged 14 commits into
mainfrom
feat/testing-spec

Conversation

@taras

@taras taras commented Jul 19, 2026

Copy link
Copy Markdown
Owner

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, a TestApi (testing operation), and an xmd test CLI 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:

import { execute } from "@executablemd/core";
import { useTesting } from "@executablemd/testing";

const tests = yield* useTesting();
const execution = yield* execute(options);
const outcome = yield* execution;          // Result<string>
const results = yield* tests.results;
  • Core exports the sole execute entrypoint, delivered through a new test-agnostic Execution context Api. Extensions decorate the execution lifecycle with Execution.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.
  • DocumentExecution completes with Result<string>: Ok(output) on success, Err(error) on document, infrastructure, or testing failure. Once execute has 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 resolves Err. Pre-handle failures may still throw. collect unwraps 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's expandInvocation hook), result collection, root testing activation (Test.around({ testing: () => true }) — the same install <Testing> performs for a subtree, making xmd test ≡ root <Testing>), and a completion policy that converts an otherwise successful execution to Err(TestFailureError) after output closes when tests failed or none were discovered. A core Err passes through unchanged and results (an immutable discovery-order snapshot) stays available after failure. One session per execution scope and one execute() per session, both enforced; every install is removed with the session's Effection scope.
  • Vocabulary registration is distinct from activation: installTestingVocabulary alone (used by xmd 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 an Err outcome.
  • <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, a raise interceptor 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 20s timebox (DI seam createTestHandlers({ timeoutMs }) for fast tests), and setup/body/teardown failure classification. A completed test returns only text segments — containment survives throwing ambient raise policies.
  • Assert first, format after: assertions run on live values (expressions evaluated over projected+current bindings — no JSON round-trip, so RegExp/undefined/object identity survive; msg is type-checked as a string, never formatted); diagnostics are built only after the outcome is fixed, under guarded fallback.
  • Durable testing records preserve replay outcomes: every completed TestResult and 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 from ExecuteOptions.stream into 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.
  • Core support fixes: ComponentInvocation carries 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.
  • Self-testing smoke suite: the smoke guide is composed from feature documents under 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/AssertStrictEquals for deterministic outputs and live bindings, anchored AssertMatch for genuinely dynamic values, AssertNotMatch only 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. Regular xmd run renders the prose guide (test examples are intentionally skipped); xmd test runs every scenario. testing/tests/smoke.test.ts is infrastructure only: useTesting() around core execute(), 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 rejects Instruction within Instruction, so composition cannot be demonstrated in-document.)
  • CLI: xmd run registers the vocabulary around execute; xmd test composes useTesting() around the same execute call. Both inspect the returned Result for exit status (0 on Ok, 1 on any Err).

Scope confirmation

  • All changed files relate to the stated purpose
  • No drive-by refactors or "while I'm here" cleanups
  • No formatting changes mixed with functional changes

New abstractions (if any)

  • Each new type/interface/class has 3+ consumers or justification
  • No speculative features ("we might need this later")

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)

  • Package: @std/assert (jsr, already resolved in deno.lock; npm alias @jsr/std__assert via the @jsr scope registry added to .npmrc/bunfig.toml)
  • Justification: specs/testing-spec.md mandates assertion components use @std/assert semantics and error types.

New tests (if any)

  • core/tests/expand-invocation.test.ts (6): fall-through, claim, handled-empty vs unhandled, raise policy, context passing
  • core/tests/source-position.test.ts (6): local/origin translation, frontmatter offset with a body-line copy inside frontmatter (defeats content-search false matches), imported components
  • core/tests/replay-stream.test.ts (6): replay-before-subscribe, replay/live boundary exactly-once, multiple subscribers, subscribe-after-close, post-close ignore
  • testing/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-only msg type check, assert-then-format regressions
  • testing/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 identity
  • testing/tests/use-testing.test.ts (10): session composition + results snapshot, results after failure, core Err preserved, one execute() 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 teardown
  • testing/tests/replay.test.ts (6): passing/failing/zero-test useTesting() 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 effects
  • testing/tests/cli.test.ts (4): xmd test exit 0/1 with piped report, zero-test exit 1, xmd run skip + exit 0
  • testing/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 events

Release notes

@executablemd/testing starts 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: first npm publish by hand, trusted publisher config, and JSR package creation. Follow-up tracked separately: the local/no-section-divider-comments oxlint rule (#109).

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

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 12 redundant comments. Inline suggestions to remove them below.

Comment thread core/src/component-api.ts
Comment thread core/src/replay-stream.ts
Comment thread core/src/replay-stream.ts
Comment thread testing/src/execute.ts Outdated
Comment thread testing/src/handlers.ts
Comment thread testing/src/test-api.ts
Comment thread testing/src/test-api.ts
Comment thread core/src/execute.ts
Comment thread testing/src/handlers.ts Outdated
Comment thread testing/src/handlers.ts Outdated
@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown

PR #108: feat: implement specs/testing-spec.md — , , assertions, xmd test

87 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

  • core/src/execute.ts:65// execute options (spec §7.1)
  • core/src/execute.ts:359// execute (spec §7.1)
  • testing/src/handlers.ts:107// deno-lint-ignore require-yield
  • testing/src/journal.ts:27// deno-lint-ignore require-yield
  • testing/src/journal.ts:45// deno-lint-ignore require-yield
  • testing/src/test-api.ts:60// deno-lint-ignore require-yield
  • testing/src/test-api.ts:62// deno-lint-ignore require-yield
  • testing/src/test-api.ts:66// deno-lint-ignore require-yield
  • testing/src/use-testing.ts:41// deno-lint-ignore require-yield

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

@taras

taras commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

Re the 12 'redundant comment' suggestions: 10 of the flagged lines are deno-lint-ignore require-yield suppression directives (the same pattern used throughout core/src/component-api.ts — one flagged instance is pre-existing code), not prose comments. The remaining two state spec constraints the code cannot show (the no-summary rule for <Testing> reports and the skip guarantee for <Test> in regular execution). Leaving them in place per Code Rule 4.

taras added 3 commits July 19, 2026 21:18
- <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
@taras
taras force-pushed the feat/testing-spec branch from 69937e3 to f378119 Compare July 20, 2026 02:24
taras added 3 commits July 19, 2026 22:27
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

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 6 redundant comments. Inline suggestions to remove them below.

Comment thread core/src/execute.ts
Comment thread testing/src/handlers.ts
Comment thread testing/src/test-api.ts
Comment thread testing/src/use-testing.ts
Comment thread testing/src/use-testing.ts
Comment thread testing/src/use-testing.ts

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 2 redundant comments. Inline suggestions to remove them below.

Comment thread testing/src/handlers.ts
Comment thread testing/src/handlers.ts
… 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

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 1 redundant comment. Inline suggestions to remove them below.

Comment thread testing/src/vocabulary.ts
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.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 3 redundant comments. Inline suggestions to remove them below.

Comment thread core/src/execute.ts
Comment thread core/src/execute.ts
Comment thread testing/src/use-testing.ts
taras added 2 commits July 20, 2026 00:08
…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

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 11 redundant comments. Inline suggestions to remove them below.

Comment thread core/src/execute.ts
Comment thread testing/src/use-testing.ts
Comment thread testing/src/vocabulary.ts
Comment thread testing/src/handlers.ts
Comment thread testing/src/assertions.ts
Comment thread testing/src/handlers.ts
Comment thread testing/src/assertions.ts
Comment thread core/src/expand.ts
Comment thread core/src/execute.ts
Comment thread core/src/execute.ts
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.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 6 redundant comments. Inline suggestions to remove them below.

Comment thread cli/src/cli.ts
Comment thread cli/src/cli.ts
Comment thread core/src/deno-compiler.ts
Comment thread core/src/execute.ts
Comment thread core/src/temp-file-compiler.ts
Comment thread testing/src/vocabulary.ts
@taras

taras commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

Triage of the b3cf9e3/8ce9430 review passes:

  • 10 of 14 flagged lines are deno-lint-ignore require-yield suppression directives (same recurring false positive as before) — kept.
  • 3 are mid-comment tail lines of multi-line explanations (containment invariant in handlers.ts, the source-position invariant check in execute.ts, root-activation equivalence in use-testing.ts); applying the single-line suggestions would corrupt load-bearing comments — kept.
  • 1 (execute.ts:359 // execute (spec §7.1)) is a genuine decorative section header, but the repo-wide divider sweep was intentionally reverted and is tracked by Enforce Code Rule 10: local/no-section-divider-comments oxlint rule with autofix #109 — removing one header piecemeal would leave the codebase inconsistent, so deferring to that issue.
  • Scope notes: testing/tests/helpers.ts has four consumers (assertions, testing-mode, use-testing, replay suites); the two console statements are cli.ts's intended stderr error paths, allow-listed in .oxlintrc.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 9 redundant comments. Inline suggestions to remove them below.

Comment thread core/src/execute.ts
Comment thread core/src/execute.ts
Comment thread testing/src/handlers.ts
Comment thread testing/src/journal.ts
Comment thread testing/src/journal.ts
Comment thread testing/src/test-api.ts
Comment thread testing/src/test-api.ts
Comment thread testing/src/test-api.ts
Comment thread testing/src/use-testing.ts
@taras
taras merged commit 9c5bccd into main Jul 20, 2026
8 checks passed
@taras
taras deleted the feat/testing-spec branch July 20, 2026 04:38
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