From d911d1105e576febfc544d82ed99e69370ea43ce Mon Sep 17 00:00:00 2001 From: Khushboo-dev-cpp <60327365+Khushboo-dev-cpp@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:59:59 +0000 Subject: [PATCH] feat: add doc tests (#63) --- .github/workflows/doctests.yml | 265 +++++++++++++++ .gitignore | 8 + doctests/blockchain-module-config.test.yaml | 209 ++++++++++++ doctests/blockchain-module-runtime.test.yaml | 321 +++++++++++++++++++ doctests/run.sh | 28 ++ 5 files changed, 831 insertions(+) create mode 100644 .github/workflows/doctests.yml create mode 100644 doctests/blockchain-module-config.test.yaml create mode 100644 doctests/blockchain-module-runtime.test.yaml create mode 100755 doctests/run.sh diff --git a/.github/workflows/doctests.yml b/.github/workflows/doctests.yml new file mode 100644 index 0000000..3dcbb10 --- /dev/null +++ b/.github/workflows/doctests.yml @@ -0,0 +1,265 @@ +name: blockchain-module Doc-Tests + +# Runs the executable blockchain-module doc-tests end-to-end via the shared +# doctest CLI: +# - blockchain-module-config.test.yaml: packages and installs THIS commit of +# the blockchain module as an .lgx, starts a logoscore daemon, loads the +# module, and generates node configs (with bootstrap peers, and with +# skip_ibd for a peerless run). +# - blockchain-module-runtime.test.yaml: takes that config and drives a real +# node lifecycle — start, chain state, wallet addresses, stop. +# Both run in one invocation so they share a single HTML report. +# +# Both specs run the node with NO bootstrap peers (skip_ibd). That is +# deliberate: a node configured with peers spends startup trying to fetch a +# chain tip from each, and aborts bootstrap if none answer — so a CI job that +# depends on a live network fails whenever the network moves, the peers rotate, +# or the node's chainsync protocol version changes. With IBD skipped the node +# comes up against its own genesis in under a second and every assertion is +# deterministic. +# +# ────────────────────────────────────────────────────────────────────────────── +# One-time setup required for the clickable report links to work: +# +# 1. Repo Settings → Pages → "Build and deployment" → Source: "Deploy from a +# branch", Branch: `gh-pages` / `(root)`. (The publish-report job creates +# the gh-pages branch on its first run.) +# 2. Nothing else — GITHUB_TOKEN already has the permissions granted below. +# +# Each run publishes the two-column HTML report to: +# https://.github.io//pr-// (pull requests) +# https://.github.io//main// (pushes to master) +# and (for PRs) posts/updates a comment with the links. +# +# Note: pull requests opened from forks get a read-only GITHUB_TOKEN, so the +# Pages push and PR comment are skipped for them — the downloadable artifact is +# still produced. +# ────────────────────────────────────────────────────────────────────────────── + +on: + pull_request: + branches: [master, main] + push: + branches: [master, main] + +concurrency: + group: doctests-${{ github.ref }} + cancel-in-progress: true + +jobs: + doctests: + name: blockchain-module doc-tests (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + # Linux only, unlike the other repos' doc-tests. The specs build the + # module's .lgx, which compiles the bundled logos_blockchain native + # library — and this repo builds for Linux exclusively (ci.yml is + # ubuntu-latest; release.yml is ubuntu-24.04 + ubuntu-24.04-arm). With no + # darwin job anywhere there is no darwin binary cache to substitute from, + # so a macOS runner compiles the whole Rust node from source every time: + # measured at ~4.5 min on ubuntu against >15 min and still going on macOS. + # + # Add macos-latest back once this repo ships a darwin build that + # populates the cache — at that point the doctest gets it nearly free. + os: [ubuntu-latest] + + runs-on: ${{ matrix.os }} + timeout-minutes: 90 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + + - name: Setup Cachix + uses: cachix/cachix-action@v15 + with: + name: logos-co + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + # Resolve the commit under test. For pull requests this is the PR's head + # commit (not the synthetic merge commit); for pushes it's the pushed + # commit. Passed to --release-for below so the doc-test packages THIS + # commit of logos-blockchain-module instead of the latest published flake. + # + # Fork PRs are the exception: their head commit lives in the fork, not in + # logos-blockchain/logos-blockchain-module, so nix could not fetch + # `github:logos-blockchain/logos-blockchain-module/`. We blank the SHA + # for forks (--release-for repo= → pins that repo to latest), so the + # doc-test still runs for fork PRs, just against master. + - name: Resolve commit under test + id: commit + shell: bash + run: | + if [ "${{ github.event_name }}" = "pull_request" ] && \ + [ "${{ github.event.pull_request.head.repo.fork }}" = "true" ]; then + echo "sha=" >> "$GITHUB_OUTPUT" + echo "Fork PR detected — doc-test will run against latest master." + else + echo "sha=${{ github.event.pull_request.head.sha || github.sha }}" >> "$GITHUB_OUTPUT" + fi + + # The runner is the shared `doctest` CLI, invoked directly via its flake + # (github:logos-co/logos-doctest). The flake bundles Python + PyYAML + # (+ rich), so no pip install step is needed. + # + # --release-for pins the {release} placeholder for logos-blockchain-module + # to the commit under test, so + # `github:logos-blockchain/logos-blockchain-module{release}` in the spec + # becomes `.../`. Every other repo URL still resolves to latest. + - name: Run blockchain-module doc-tests + run: | + # Both specs run in one invocation so they share a single --report. + # --continue-on-fail so the run walks every step and the published + # report is complete. The job still fails (non-zero exit) if any step + # failed; this only changes whether we stop early. + # + # No --output-dir: it only supports a single spec. Without it the specs + # run in per-spec temp workdirs and still land in one --report. + + nix run github:logos-co/logos-doctest -- run \ + doctests/blockchain-module-config.test.yaml \ + doctests/blockchain-module-runtime.test.yaml \ + --verbose \ + --continue-on-fail \ + --release-for logos-blockchain-module=${{ steps.commit.outputs.sha }} \ + --report "${{ runner.temp }}/blockchain-doctest-report.html" + + - name: Stage report for upload + if: always() + shell: bash + run: | + mkdir -p report-out + # Name it index.html so the published directory URL renders directly. + if [ -f "${{ runner.temp }}/blockchain-doctest-report.html" ]; then + cp "${{ runner.temp }}/blockchain-doctest-report.html" report-out/index.html + else + echo "

No report produced

" > report-out/index.html + fi + + - name: Upload blockchain-module execution report + if: always() + uses: actions/upload-artifact@v4 + with: + name: blockchain-doctest-report-${{ matrix.os }} + path: report-out/index.html + if-no-files-found: warn + + - name: Verify markdown generation + run: | + for spec in blockchain-module-config blockchain-module-runtime; do + nix run github:logos-co/logos-doctest -- generate \ + "doctests/$spec.test.yaml" \ + --release-for logos-blockchain-module=${{ steps.commit.outputs.sha }} \ + -o "/tmp/$spec.md" + test -s "/tmp/$spec.md" + done + echo "Generated markdown successfully" + + publish-report: + name: Publish report to GitHub Pages + needs: doctests + # Run even when tests fail — a failing run is exactly when you want to open + # the report. Skip on forks, where GITHUB_TOKEN can't push or comment. + if: ${{ always() && github.event.pull_request.head.repo.fork != true }} + runs-on: ubuntu-latest + + permissions: + contents: write # push to the gh-pages branch + pull-requests: write # post/update the PR comment + + # Serialize Pages pushes so two refs can't race on the gh-pages branch. + concurrency: + group: gh-pages-publish + cancel-in-progress: false + + steps: + - name: Download all reports + uses: actions/download-artifact@v4 + with: + path: artifacts + # No `name:` → downloads every artifact into artifacts//... + + - name: Arrange site directory + id: arrange + shell: bash + run: | + set -euo pipefail + if [ "${{ github.event_name }}" = "pull_request" ]; then + BASE="pr-${{ github.event.pull_request.number }}" + else + BASE="main" + fi + echo "base=$BASE" >> "$GITHUB_OUTPUT" + + mkdir -p "site/$BASE" + found="" + for os in ubuntu-latest; do + src="artifacts/blockchain-doctest-report-$os/index.html" + if [ -f "$src" ]; then + mkdir -p "site/$BASE/$os" + cp "$src" "site/$BASE/$os/index.html" + found="$found $os" + fi + done + echo "found=$found" >> "$GITHUB_OUTPUT" + + # Landing page for this ref linking to each OS report. + { + echo "" + echo "blockchain-module doc-test reports — $BASE" + echo "" + echo "

blockchain-module doc-test reports

" + echo "

$BASE · commit ${GITHUB_SHA::7}

    " + for os in ubuntu-latest; do + if [ -d "site/$BASE/$os" ]; then + echo "
  • $os
  • " + fi + done + echo "
" + } > "site/$BASE/index.html" + + - name: Deploy to gh-pages + if: steps.arrange.outputs.found != '' + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./site + keep_files: true # don't wipe other PRs' directories + commit_message: "Publish blockchain-module doc-test report for ${{ steps.arrange.outputs.base }} (${{ github.sha }})" + + - name: Comment on PR with report links + if: ${{ github.event_name == 'pull_request' && steps.arrange.outputs.found != '' }} + uses: actions/github-script@v7 + with: + script: | + const base = "${{ steps.arrange.outputs.base }}"; + const owner = context.repo.owner; + const repo = context.repo.repo; + const root = `https://${owner}.github.io/${repo}/${base}`; + const oses = "${{ steps.arrange.outputs.found }}".trim().split(/\s+/).filter(Boolean); + + const links = oses.map(os => `- [\`${os}\` report](${root}/${os}/)`).join("\n"); + const marker = ""; + const body = + `${marker}\n` + + `### 📊 blockchain-module doc-test report\n\n` + + `This commit of the blockchain module, packaged as an \`.lgx\` and ` + + `driven through a logoscore daemon — config generation and a full ` + + `node lifecycle — rendered alongside the commands actually run and ` + + `their output (updated each run, commit \`${context.sha.slice(0,7)}\`):\n\n` + + `${links}\n\n` + + `_Pages can take a minute to update after the run finishes._`; + + const { data: comments } = await github.rest.issues.listComments({ + owner, repo, issue_number: context.issue.number, per_page: 100, + }); + const existing = comments.find(c => c.body && c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); + } else { + await github.rest.issues.createComment({ owner, repo, issue_number: context.issue.number, body }); + } diff --git a/.gitignore b/.gitignore index eb76a3c..0b865e1 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,11 @@ state/ # AI .codex +# Doc-test output: generated artifacts are disposable, but keep the rendered +# .md that the docs embed. +doctests/outputs/* +!doctests/outputs/*.md +# Disposable scratch dir for doc-test previews (kept separate so it never +# clobbers the committed .md in doctests/outputs). +doctests/preview-outputs +doctest-local diff --git a/doctests/blockchain-module-config.test.yaml b/doctests/blockchain-module-config.test.yaml new file mode 100644 index 0000000..271baad --- /dev/null +++ b/doctests/blockchain-module-config.test.yaml @@ -0,0 +1,209 @@ +name: "Generating and Inspecting a Logos Blockchain Node Config" +output: blockchain-module-config.md +release: "" + +intro: | + `blockchain_module` wraps the [`logos-blockchain`](https://github.com/logos-blockchain/logos-blockchain) + node as a Logos module. Before a node can run it needs a **user config** — a YAML + file describing the keystore, the network ports, the bootstrap peers to sync + from, and where the node keeps its state, database and logs. + + The module generates that file for you through `generate_user_config`, so the + operator never has to hand-write YAML. This doc-test drives that call through + the headless `logoscore` daemon and inspects what comes out, then shows what + happens when you try to generate a second config into the same instance. + + > No node is started here — config generation is entirely local and needs no + > network. For starting and stopping a node see the companion + > [`blockchain-module-runtime`](./blockchain-module-runtime.md) doc-test. + +what_you_build: "A generated `user_config.yaml` for a Logos blockchain node, produced through the module's `generate_user_config` call and inspected for the values that were asked for." + +what_you_learn: + - How a Logos module is packaged as `.lgx` and installed with `lgpm` + - How to call a module's methods headlessly through the `logoscore` daemon + - What `generate_user_config` writes, and which knobs it accepts + - Why a second generation into the same instance is refused, and what to use instead + - Why the generated config's paths land under the module's own per-instance directory + +prerequisites: + - | + **Nix** with flakes enabled (see [nixos.org](https://nixos.org/download.html)). + - "**A Linux or macOS machine.** Nothing here needs a display or a network peer." + +sections: + - title: "Build the Logos daemon" + step: true + text: | + Build the Logos runtime CLI from its published flake. The result outputs a + binary named `logoscore` under a symlinked directory named `./logos`. + `logoscore` is the headless frontend for + [`logos-liblogos`](https://github.com/logos-co/logos-liblogos) — it brings in + the whole module-runtime stack we need to load and call the module. + steps: + - title: "Build the CLI" + run: "nix build 'github:logos-co/logos-logoscore-cli' --out-link ./logos" + code_block: | + nix build 'github:logos-co/logos-logoscore-cli' --out-link ./logos + check_file: "logos/bin/logoscore" + post_text: | + The build produces `logos/bin/logoscore` plus bundled runtime libraries + and a `logos/modules/` directory containing the built-in + `capability_module` (required for the auth handshake when loading + modules). + + - title: "Build the lgpm package manager" + step: true + text: | + `lgpm` installs `.lgx` packages into a modules directory so the daemon can + discover them. + steps: + - title: "Build lgpm" + run: "nix build 'github:logos-co/logos-package-manager' --out-link ./lgpm" + code_block: | + nix build 'github:logos-co/logos-package-manager' --out-link ./lgpm + check_file: "lgpm/bin/lgpm" + + - title: "Build and install the blockchain module" + step: true + text: | + The module's flake exposes an `.lgx` package — the Logos distribution format. + Building it compiles the Qt plugin *and* the bundled `logos_blockchain` + native library it links against, so this step is the slow one. + steps: + - title: "Build the .lgx package" + run: "nix build 'github:logos-blockchain/logos-blockchain-module{release}#lgx' --out-link ./blockchain-lgx" + code_block: | + nix build 'github:logos-blockchain/logos-blockchain-module{release}#lgx' --out-link ./blockchain-lgx + + - title: "Install it into a modules directory" + text: "Install the package so the daemon can find it:" + run: "./lgpm/bin/lgpm --modules-dir ./modules install --dir ./blockchain-lgx" + code_block: | + lgpm --modules-dir ./modules install --dir ./blockchain-lgx + expect_contains: + - "blockchain_module" + check_file: "modules/blockchain_module/manifest.json" + + - title: "Load the module in the daemon" + step: true + text: | + Start the daemon pointed at the modules directory, then load + `blockchain_module`. Loading only brings the plugin into the process — it + does **not** start a blockchain node. + steps: + - title: "Start the daemon" + run: "./logos/bin/logoscore stop >/dev/null 2>&1; sleep 2; ./logos/bin/logoscore daemon --modules-dir ./modules --persistence-path ./data >/dev/null 2>&1 &" + code_block: | + logoscore daemon --modules-dir ./modules --persistence-path ./data + + - title: "Load the module" + run: "sleep 8; ./logos/bin/logoscore load-module blockchain_module" + code_block: | + logoscore load-module blockchain_module + expect_contains: + - "blockchain_module" + + - title: "Inspect the methods it exposes" + text: | + `module-info` lists the `Q_INVOKABLE` methods — the same names you can + `call`. Note `generate_user_config`, `start`, `stop`, and the + `wallet_*` family: + run: "./logos/bin/logoscore module-info blockchain_module" + code_block: | + logoscore module-info blockchain_module + expect_contains: + - "generate_user_config" + - "start" + - "stop" + + - title: "Generate a config with bootstrap peers" + step: true + text: | + `generate_user_config` takes a JSON object of arguments. Every field is + optional — an empty object produces a working default config. Here we pass + the two that matter most for joining a network: the bootstrap peers to sync + from, and the ports to listen on. + + `use_persistence_paths` routes the config — and the node's `state`, `db` + and `logs` directories — under the module's own per-instance directory, + which is always writable. That is the safe default. + steps: + - title: "Write the arguments" + file: + path: gen-args.json + content: | + { + "initial_peers": [ + "/ip4/65.109.51.37/udp/3000/quic-v1/p2p/12D3KooWFrouXfmrR4nsLMtE7wu15DoMJ6VtoUtHinREZCvbWHar" + ], + "net_port": 3000, + "blend_port": 3001, + "output": "user_config.yaml", + "use_persistence_paths": true + } + + - title: "Generate the config" + run: "./logos/bin/logoscore call blockchain_module generate_user_config @gen-args.json" + code_block: | + logoscore call blockchain_module generate_user_config @gen-args.json + expect_contains: + - '"success":true' + - "user_config.yaml" + post_text: | + The call returns the absolute path of the file it wrote — that path is + what you hand to `start`, as the runtime doc-test does. + + `output` is worth passing even though it is optional. Omit it and the + call still reports success but returns an **empty** path, leaving you + nothing to start the node with. + + - title: "Generating a second config into the same instance" + step: true + text: | + A natural next move is to generate a *second* config — say a peerless one + with `skip_ibd: true` — alongside the first. That does **not** work, and the + error is worth seeing because it is the same one operators hit when they + re-run generation over an existing setup. + + The keystore is created once per instance directory. A second + `generate_user_config` against the same instance refuses rather than + overwriting it: + steps: + - title: "Write the arguments" + file: + path: gen-args-solo.json + content: | + { + "skip_ibd": true, + "net_port": 3100, + "blend_port": 3101, + "output": "solo-user-config.yaml", + "use_persistence_paths": true + } + + - title: "Try to generate a second config" + run: "./logos/bin/logoscore call blockchain_module generate_user_config @gen-args-solo.json" + code_block: | + logoscore call blockchain_module generate_user_config @gen-args-solo.json + expect_contains: + - "Keystore file exists. Use `update` command." + post_text: | + Refusing is the right behaviour — silently regenerating a keystore would + discard the keys the previous config's node identity depends on. To + change an existing setup use `update_user_config`; to get a genuinely + separate configuration, use a separate instance directory. + + This is also why `skip_ibd` is demonstrated in the companion + [`blockchain-module-runtime`](./blockchain-module-runtime.md) doc-test + rather than here: that spec generates exactly one config, with + `skip_ibd: true`, and then starts a node from it. + + - title: "Shut down" + step: true + text: "Stop the daemon. No node was ever started, so there is nothing else to clean up." + steps: + - title: "Stop the daemon" + run: "./logos/bin/logoscore stop" + code_block: | + logoscore stop diff --git a/doctests/blockchain-module-runtime.test.yaml b/doctests/blockchain-module-runtime.test.yaml new file mode 100644 index 0000000..891d184 --- /dev/null +++ b/doctests/blockchain-module-runtime.test.yaml @@ -0,0 +1,321 @@ +name: "Starting and Stopping a Logos Blockchain Node" +output: blockchain-module-runtime.md +release: "" + +intro: | + This doc-test takes the config produced in + [`blockchain-module-config`](./blockchain-module-config.md) and actually runs a + node with it: start, query the chain state, read the wallet's known addresses, + and stop again. + + It runs the node **with no bootstrap peers** (`skip_ibd: true`). That is + deliberate. A node configured with peers spends its startup trying to fetch a + chain tip from each one, and if none answer it aborts bootstrap — so a test + that depends on a live network is a test that fails whenever the network moves, + the peers rotate, or the node's chainsync protocol version changes. With IBD + skipped the node comes up against its own genesis state in well under a second + and every assertion below is deterministic. + + What that buys you is coverage of the whole local lifecycle: every service + reaching ready, the wallet initialising, the chain state being queryable, and a + clean shutdown. What it does not cover is syncing from a real network — see the + note at the end. + +what_you_build: "A running Logos blockchain node, started headlessly from a generated config, queried for its chain and wallet state, and shut down cleanly." + +what_you_learn: + - How to start and stop a blockchain node through the module's API + - Which services the node brings up, and what "ready" looks like for each + - How to read chain state with `get_cryptarchia_info` + - How the wallet exposes its known addresses and their balances + - Why skipping IBD is the right choice for a reproducible test, and what it leaves untested + +prerequisites: + - | + **Nix** with flakes enabled (see [nixos.org](https://nixos.org/download.html)). + - "**A Linux or macOS machine.** No display and no network peers are required." + +sections: + - title: "Build the tools and install the module" + step: true + text: | + Same preparation as the [config doc-test](./blockchain-module-config.md): + the runtime CLI, the package manager, and this commit of the module + packaged as an `.lgx`. Building the `.lgx` compiles the bundled + `logos_blockchain` native library, so it is the slow step. + steps: + - title: "Build the Logos daemon CLI" + run: "nix build 'github:logos-co/logos-logoscore-cli' --out-link ./logos" + code_block: | + nix build 'github:logos-co/logos-logoscore-cli' --out-link ./logos + check_file: "logos/bin/logoscore" + + - title: "Build lgpm" + run: "nix build 'github:logos-co/logos-package-manager' --out-link ./lgpm" + code_block: | + nix build 'github:logos-co/logos-package-manager' --out-link ./lgpm + check_file: "lgpm/bin/lgpm" + + - title: "Build the module's .lgx package" + run: "nix build 'github:logos-blockchain/logos-blockchain-module{release}#lgx' --out-link ./blockchain-lgx" + code_block: | + nix build 'github:logos-blockchain/logos-blockchain-module{release}#lgx' --out-link ./blockchain-lgx + + - title: "Install it into a modules directory" + run: "./lgpm/bin/lgpm --modules-dir ./modules install --dir ./blockchain-lgx" + code_block: | + lgpm --modules-dir ./modules install --dir ./blockchain-lgx + expect_contains: + - "blockchain_module" + check_file: "modules/blockchain_module/manifest.json" + + - title: "Start the daemon and load the module" + step: true + text: | + Run the daemon and load the plugin. Loading brings the plugin into the + process — it does **not** start a blockchain node. + steps: + - title: "Start the daemon" + run: "./logos/bin/logoscore stop >/dev/null 2>&1; sleep 2; ./logos/bin/logoscore daemon --modules-dir ./modules --persistence-path ./data >/dev/null 2>&1 &" + code_block: | + logoscore daemon --modules-dir ./modules --persistence-path ./data + + - title: "Load the module" + run: "sleep 8; ./logos/bin/logoscore load-module blockchain_module" + code_block: | + logoscore load-module blockchain_module + expect_contains: + - "blockchain_module" + + - title: "Confirm the node is not running yet" + step: true + text: | + Every node-dependent call guards on a running node and reports so plainly. + This is worth seeing before starting, because it is the error you will meet + most often in practice: + steps: + - title: "Ask for chain info with no node" + run: "./logos/bin/logoscore call blockchain_module get_cryptarchia_info" + code_block: | + logoscore call blockchain_module get_cryptarchia_info + expect_contains: + - "The node is not running." + + - title: "Generate a solo config" + step: true + text: | + Generate a config with no bootstrap peers. `skip_ibd` empties the IBD peer + list, so the node will skip Initial Block Download rather than attempt to + sync from anyone. + steps: + - title: "Write the arguments" + file: + path: runtime-args.json + content: | + { + "skip_ibd": true, + "net_port": 3200, + "blend_port": 3201, + "output": "user_config.yaml", + "use_persistence_paths": true + } + + - title: "Generate it" + run: "./logos/bin/logoscore call blockchain_module generate_user_config @runtime-args.json" + code_block: | + logoscore call blockchain_module generate_user_config @runtime-args.json + expect_contains: + - '"success":true' + - "user_config.yaml" + post_text: | + `output` matters here. Omit it and the call still succeeds but returns an + empty path, leaving you nothing to hand to `start`. Give it a relative + path and the module resolves it under the instance's persistence + directory and returns the absolute path it wrote. + + - title: "Confirm skip_ibd emptied the peer list" + text: | + `skip_ibd` writes an empty IBD peer list into the config, which is what + makes the node skip bootstrap rather than try to sync: + run: "CFG=$(find . $HOME/.logoscore -name user_config.yaml -path '*blockchain_module*' 2>/dev/null | head -1); echo using $CFG; grep -A1 'ibd:' $CFG" + code_block: | + grep -A1 'ibd:' /user_config.yaml + expect_contains: + - "peers: []" + + - title: "Shorten the bootstrap period" + text: | + A generated config carries `prolonged_bootstrap_period: '3600.000000000'` + — the node stays in `Bootstrapping` mode for a full hour before + switching to `Online`. That is sensible for joining a real network and + useless for a test, so patch it down. `generate_user_config` does not + expose this knob, so we edit the YAML directly. + run: "CFG=$(find . $HOME/.logoscore -name user_config.yaml -path '*blockchain_module*' 2>/dev/null | head -1); sed \"s/prolonged_bootstrap_period: '3600.000000000'/prolonged_bootstrap_period: '5.000000000'/\" $CFG > $CFG.tmp && mv $CFG.tmp $CFG && grep -n prolonged_bootstrap_period $CFG" + code_block: | + # sed -i differs between GNU and BSD/macOS, so write and move instead. + sed "s/prolonged_bootstrap_period: '3600.000000000'/prolonged_bootstrap_period: '5.000000000'/" \ + /user_config.yaml > tmp && mv tmp /user_config.yaml + expect_contains: + - "prolonged_bootstrap_period: '5.000000000'" + + - title: "Start the node" + step: true + text: | + `start` takes the config path and a deployment name. Passing an empty + deployment uses the built-in default. + + The node brings its services up in order — tracing, storage, network, + cryptarchia, wallet, mempool. Because IBD is skipped there is no bootstrap + phase to wait through, so `start` returns once the runtime is up. + steps: + - title: "Start it" + run: "CFG=$(find . $HOME/.logoscore -name user_config.yaml -path '*blockchain_module*' 2>/dev/null | head -1); ./logos/bin/logoscore call blockchain_module start $CFG ''" + code_block: | + logoscore call blockchain_module start /user_config.yaml "" + expect_contains: + - '"success":true' + post_text: | + The daemon's output records the startup sequence. Three lines are worth + knowing, because they are the ones that distinguish a healthy peerless + start from the failures you meet in practice: + + ``` + chain::service: genesis time is already in the past: finishing AwaitingGenesisTime phase with no-op + bootstrap::ibd: Skipping IBD as no peers are configured + chain_network_service: Initial Block Download completed successfully + ``` + + The first confirms the generated config's genesis is already live — a + config whose genesis is in the *future* parks the node in + `AwaitingGenesisTime` indefinitely, with no error to explain it. The + second and third are `skip_ibd` doing its job: bootstrap is bypassed + rather than attempted and failed. + + - title: "Query the chain" + step: true + text: | + With the node up, `get_cryptarchia_info` reports the consensus state: the + current tip, the last immutable block, and the slot. On a freshly started + node with nothing synced, tip and LIB are both genesis. + steps: + - title: "Read chain state" + run: "./logos/bin/logoscore call blockchain_module get_cryptarchia_info" + code_block: | + logoscore call blockchain_module get_cryptarchia_info + expect_contains: + - '"success":true' + - '"mode\":\"Bootstrapping' + post_text: | + The reply carries `height`, `slot`, `tip`, `lib` and `mode`. Immediately + after start the mode is `Bootstrapping`. + + - title: "Wait for the chain to come Online" + text: | + After the (shortened) prolonged-bootstrap period the mode flips to + `Online`. This is the transition the blend service is waiting on when it + logs *"Waiting for chain to become Online mode"*. + run: "sleep 15 && ./logos/bin/logoscore call blockchain_module get_cryptarchia_info" + code_block: | + logoscore call blockchain_module get_cryptarchia_info + expect_contains: + - '"mode\":\"Online' + post_text: | + Note `height` and `slot` are still `0`. A node with no peers reaches + `Online` but does not by itself start producing blocks — see the closing + section. + + - title: "Read the wallet" + step: true + text: | + The wallet is initialised from the keystore in the generated config, so it + knows its addresses immediately — no sync required. Balances are a separate + lookup against the ledger, and on an unsynced chain they reflect genesis + state only. + steps: + - title: "List known addresses" + run: "./logos/bin/logoscore call blockchain_module wallet_get_known_addresses" + code_block: | + logoscore call blockchain_module wallet_get_known_addresses + expect_contains: + - '"success":true' + post_text: | + Five addresses, returned as a JSON array. These are what the module's UI + lists in its Accounts panel, and the ones it then queries balances for + one at a time. + + - title: "Ask for a balance before the chain has synced" + text: | + Feed one of those addresses straight back into `wallet_get_balance`. + On a node that has not synced, the call **fails** — even though the + address came from the module's own list a moment earlier: + run: "./logos/bin/logoscore call blockchain_module wallet_get_balance \"$(./logos/bin/logoscore call blockchain_module wallet_get_known_addresses | grep -o '[0-9a-f]\\{64\\}' | head -1)\"" + code_block: | + ADDR=$(logoscore call blockchain_module wallet_get_known_addresses | grep -o '[0-9a-f]\{64\}' | head -1) + logoscore call blockchain_module wallet_get_balance "$ADDR" + expect_contains: + - "Unknown wallet address." + post_text: | + Two different notions of "known" are in play: the wallet holds the + *key*, but the *ledger* has no entry for that address until the chain + has progressed far enough to contain one. The error wording describes + the second and reads like the first, which is why it looks — wrongly — + like a malformed address. + + This is asserted here deliberately. It is the current behaviour, it is + reachable in seconds without a network, and it is the single most + confusing thing a new operator meets. If the node later returns `0` or a + distinct "not synced" error instead, this assertion should fail and be + updated — which is exactly what a doc-test is for. + + - title: "Stop the node" + step: true + text: "Shut the node down. `stop` is idempotent in the sense that stopping an already-stopped node reports plainly rather than failing hard." + steps: + - title: "Stop it" + run: "./logos/bin/logoscore call blockchain_module stop" + code_block: | + logoscore call blockchain_module stop + expect_contains: + - '"success":true' + + - title: "Confirm it is down" + run: "./logos/bin/logoscore call blockchain_module get_cryptarchia_info" + code_block: | + logoscore call blockchain_module get_cryptarchia_info + expect_contains: + - "The node is not running." + + - title: "Stop the daemon" + run: "./logos/bin/logoscore stop" + code_block: | + logoscore stop + + - title: "What this does not cover" + text: | + Skipping IBD keeps this doc-test fast and deterministic. What it leaves out + was established by running it, not assumed: + + - **The chain does not advance.** A peerless node reaches `Online` and then + stays at `height: 0`, `slot: 0` indefinitely — observed over several + minutes. Genesis being in the past and bootstrap completing are not + sufficient to make a lone node produce blocks. So **block streams, + non-zero balances and transfers cannot be asserted here**, and this + doc-test does not pretend to. + - **Syncing from peers.** Bootstrap, block download and the chainsync + protocol handshake are never exercised. A node whose peers speak a + different chainsync version fails with `AllPeersFailed` — a real and + common failure, reachable only against a live network. + + Covering transactions therefore needs either a multi-node fixture or + whatever the node uses to make a single node leader-eligible. The node's own + cucumber suite (`tests/src/cucumber`) already runs manual-node scenarios + with deployment overrides such as + `time.chain_start_time = now_plus_seconds(0)`, and is the better place to + look before attempting it here. + + Two knobs that are *not* reachable through `generate_user_config` and had to + be patched into the YAML directly — worth exposing on the FFI if this + pattern is repeated: + + - `prolonged_bootstrap_period` (the hour-long `Bootstrapping` hold) + - `time.chain_start_time` (deployment-side, controls genesis) diff --git a/doctests/run.sh b/doctests/run.sh new file mode 100755 index 0000000..73307c8 --- /dev/null +++ b/doctests/run.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# +# Run this repo's doc-tests end-to-end and regenerate their Markdown into +# ./outputs/. The runner is the shared `doctest` CLI +# (https://github.com/logos-co/logos-doctest), invoked via its flake. +# +# Override the runner with DOCTEST, e.g. to use a local checkout: +# DOCTEST="nix run path:../../logos-doctest --" ./run.sh +# +set -euo pipefail +cd "$(dirname "$0")" + +read -r -a DOCTEST <<< "${DOCTEST:-nix run github:logos-co/logos-doctest --}" + +if [ -e outputs ]; then chmod -R u+w outputs 2>/dev/null || true; fi +rm -rf outputs && mkdir -p outputs + +for spec in *.test.yaml; do + name="$(basename "${spec%.test.yaml}")" + echo "==> Running ${spec}" + "${DOCTEST[@]}" run "${spec}" --verbose --output-dir ./outputs/ + echo "==> Generating outputs/${name}.md" + "${DOCTEST[@]}" generate "${spec}" -o "outputs/${name}.md" +done + +echo "==> Cleaning build artifacts from outputs/ (keeps .md and images/)" +"${DOCTEST[@]}" clean ./outputs --verbose 2>/dev/null || true +echo "==> Done."