add doctests

This commit is contained in:
Iuri Matias 2026-06-15 11:06:59 -04:00
parent fc1439cf16
commit bff887b060
4 changed files with 962 additions and 0 deletions

238
.github/workflows/doctests.yml vendored Normal file
View File

@ -0,0 +1,238 @@
name: blockchain-module Doc-Tests
# Runs the executable blockchain-module doc-test
# (doctests/blockchain-module-runtime.test.yaml) end-to-end via the shared
# doctest CLI: builds logoscore and lgpm, packages and installs THIS commit of
# the blockchain module as an .lgx, starts the daemon, loads the module, calls
# its methods, and asserts on the output.
#
# ──────────────────────────────────────────────────────────────────────────────
# 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://<owner>.github.io/<repo>/pr-<N>/<os>/ (pull requests)
# https://<owner>.github.io/<repo>/main/<os>/ (pushes to main/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. PRs from branches in this repo get the full clickable links.
# ──────────────────────────────────────────────────────────────────────────────
on:
pull_request:
branches: [master, main]
push:
branches: [master, main]
workflow_dispatch:
concurrency:
group: doctests-${{ github.ref }}
cancel-in-progress: true
jobs:
doctests:
name: blockchain-module doc-tests (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 120
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/<sha>`. 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 `.../<sha>` — the doc-test packages and exercises this PR/push
# rather than master. Every other repo URL still resolves to latest.
- name: Run blockchain-module runtime doc-test
run: |
# --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.
nix run github:logos-co/logos-doctest -- run \
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 "<h1>No report produced</h1>" > 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-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>/...
- 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 macos-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 "<!doctype html><meta charset=utf-8>"
echo "<title>blockchain-module doc-test reports — $BASE</title>"
echo "<style>body{font:16px system-ui;margin:40px;max-width:640px}a{color:#2563eb}</style>"
echo "<h1>blockchain-module doc-test reports</h1>"
echo "<p><strong>$BASE</strong> · commit <code>${GITHUB_SHA::7}</code></p><ul>"
for os in ubuntu-latest macos-latest; do
if [ -d "site/$BASE/$os" ]; then
echo "<li><a href=\"./$os/\">$os</a></li>"
fi
done
echo "</ul>"
} > "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 = "<!-- blockchain-doctest-report-links -->";
const body =
`${marker}\n` +
`### 📊 blockchain-module doc-test report\n\n` +
`This commit of the blockchain module, packaged as an \`.lgx\` and run ` +
`through a logoscore daemon — 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 });
}

View File

@ -0,0 +1,310 @@
name: "Running This Blockchain Module Against logoscore"
output: blockchain-module-runtime.md
release: ""
intro: |
`logos-blockchain-module` is a Logos `core` module that wraps the
[logos-blockchain](https://github.com/logos-blockchain/logos-blockchain) C
bindings — a full Cryptarchia consensus node with wallet, blend, keystore and
block-explorer APIs — and ships the zk circuit binaries the node needs at
runtime. This doc-test exercises **this** blockchain-module commit end-to-end
through the headless `logoscore` runtime:
1. Build the `logoscore` CLI and the `lgpm` local package manager from their
published flakes. `logoscore` is the headless frontend for `logos-liblogos`,
so building it brings in the whole module-runtime stack (`logos_host`,
`liblogos_core`, the IPC layer).
2. Build **this** blockchain module as an installable `.lgx` package straight
from its own flake's `#lgx` output, **pinned to the commit under test** — so
the module you run is built from exactly what is checked out here, not the
latest published release.
3. Install the `.lgx` into a `./modules` directory with `lgpm`.
4. Start `logoscore` in daemon mode (`-D`), load `liblogos_blockchain_module`, introspect
it with `module-info`, and call several of its methods — verifying the module
actually runs and round-trips real values through the logos-blockchain C
library.
Joining the devnet means dialing real peers, NTP sync and zk proving — none of
which is reproducible in CI — so this doc-test deliberately stays **offline**.
We exercise the methods that do real, deterministic work without a running node
(generating a node user-config and its keystore, deriving the peer id,
re-syncing the config from the keystore) and we confirm the node-backed methods
(wallet, explorer, consensus) are wired up and callable by asserting on the
well-defined `The node is not running` response they return before a node is
started. Starting an actual node is covered by the UI app and the developer
guide.
Because the module is built from the commit under test and then loaded and
called through a real `logoscore` daemon, a green run is real evidence that this
change keeps the blockchain module loadable and callable.
what_you_build: "This `liblogos_blockchain_module`, packaged as `.lgx`, installed with `lgpm`, and called through a `logoscore` daemon."
what_you_learn:
- How to build the `logoscore` runtime and the `lgpm` package manager from their flakes
- How a module's flake exposes a ready-to-install `.lgx` via its `#lgx` output
- How to install an `.lgx` into a modules directory with `lgpm`
- How to start the `logoscore` daemon, load a module, introspect it, and call its methods
- How to generate a node user-config and keystore, and derive the peer id, offline
- How to re-sync a user config from its keystore with `update_user_config`
- How the node-backed methods report a clear error until a node is started
- How to shut the daemon down and confirm it has exited
prerequisites:
- |
**Nix** with flakes enabled. Install from [nixos.org](https://nixos.org/download.html), then enable flakes:
```bash
mkdir -p ~/.config/nix
echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
```
Verify: `nix flake --help >/dev/null 2>&1 && echo "Flakes enabled"`
- "**A Linux or macOS machine.**"
sections:
- title: "Build logoscore"
step: true
text: |
Build the `logoscore` CLI from its published flake. The result is symlinked to
`./logos/`. `logoscore` is the headless frontend for `logos-liblogos`, so this
one build brings in the whole module-runtime stack the daemon needs.
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 and scans what is
installed. Build it from the `logos-package-manager` flake and link it as
`./lgpm`.
steps:
- title: "Build lgpm"
run: "nix build 'github:logos-co/logos-package-manager#cli' -o lgpm"
check_file: "lgpm/bin/lgpm"
post_text: "The executable is at `./lgpm/bin/lgpm`."
- title: "Build and install this blockchain module"
step: true
text: |
Build **this** blockchain module's `.lgx` straight from its flake's `#lgx`
output and install it into a local `./modules` directory with `lgpm`. Every
module built with
[`logos-module-builder`](https://github.com/logos-co/logos-module-builder)
exposes a ready-to-install `#lgx`.
> The `{release}` in the URL is what pins the build to a specific commit: the
> doc-test runner expands it to a concrete ref. Locally that is this
> checkout's `HEAD` (see `run.sh`); in CI it is the commit being tested. With
> no pin it falls back to the latest `master`.
steps:
- title: "Build the module's .lgx"
text: |
Build the `#lgx` output and link it as `./blockchain-lgx`. (This compiles
the module, the logos-blockchain Rust node and its zk circuits through
Nix, so the first build is slow.)
run: "nix build 'github:logos-blockchain/logos-blockchain-module{release}#lgx' -o blockchain-lgx"
code_block: |
# From inside the clone this is simply: nix build '.#lgx'
nix build 'github:logos-blockchain/logos-blockchain-module{release}#lgx' -o blockchain-lgx
post_text: "The `.lgx` package is now under `./blockchain-lgx/`:"
extra_run:
run: "ls blockchain-lgx/*.lgx"
- title: "Seed the modules directory with the bundled capability module"
text: |
`liblogos_blockchain_module` is loaded through the host's capability layer, so the
modules directory also needs the `capability_module` that ships with
`logoscore`. Copy it across first.
run: |
mkdir -p modules
cp -RL ./logos/modules/. ./modules/
check_file: "modules/capability_module/manifest.json"
- title: "Install the .lgx with lgpm"
text: |
Install the freshly-built package into `./modules`. `liblogos_blockchain_module` is
a `core` module, so it goes to `--modules-dir`. The package is unsigned
(a local dev build), so we pass `--allow-unsigned`.
run: "./lgpm/bin/lgpm --modules-dir ./modules --allow-unsigned install --file blockchain-lgx/*.lgx"
expect_contains:
- "Installed to:"
- title: "Confirm the install"
text: "Scan the directory and confirm the module landed:"
run: "./lgpm/bin/lgpm --modules-dir ./modules list"
expect_contains:
- "liblogos_blockchain_module"
check_file: "modules/liblogos_blockchain_module/manifest.json"
- title: "Run the daemon and call the module"
step: true
text: |
Start `logoscore` in daemon mode pointed at `./modules`, then use the client
subcommands to load `liblogos_blockchain_module`, introspect it, and call several of
its methods. Daemon output is captured in `logs.txt`.
steps:
- title: "Start the daemon"
text: |
Start logoscore in daemon mode in the background, capturing output to
`logs.txt`:
run: "sh -c './logos/bin/logoscore -D -m ./modules > logs.txt 2>&1 &'"
code_block: "logoscore -D -m ./modules > logs.txt &"
post_text: |
The `-D` flag starts the daemon. The client subcommands below connect to
this running process via the config written under `~/.logoscore/`.
- run: "sleep 3"
- title: "Inspect the startup log"
text: "Review the daemon's startup output:"
run: "cat logs.txt"
- title: "Check daemon status"
text: "Verify the daemon is running:"
run: "./logos/bin/logoscore status"
code_block: "logoscore status"
- title: "List discovered modules"
text: "`liblogos_blockchain_module` should be visible in the scan directory:"
run: "./logos/bin/logoscore list-modules"
code_block: "logoscore list-modules"
expect_contains:
- "liblogos_blockchain_module"
- title: "Load the module"
text: "Load `liblogos_blockchain_module` into the running daemon:"
run: "./logos/bin/logoscore load-module liblogos_blockchain_module"
code_block: "logoscore load-module liblogos_blockchain_module"
expect_contains:
- "liblogos_blockchain_module"
- title: "Confirm the module is loaded"
text: |
Re-run `status`; the module that was `not_loaded` before now reports
`loaded`:
run: "./logos/bin/logoscore status"
code_block: "logoscore status"
expect_contains:
- "liblogos_blockchain_module"
- '"status":"loaded"'
- title: "Introspect the module with module-info"
text: |
`module-info` lists the `Q_INVOKABLE` methods the module exposes — the
same methods you can `call`:
run: "./logos/bin/logoscore module-info liblogos_blockchain_module"
code_block: "logoscore module-info liblogos_blockchain_module"
expect_contains:
- "liblogos_blockchain_module"
- "generate_user_config"
- "get_peer_id"
- "get_cryptarchia_info"
- title: "Generate a node user-config"
text: |
`generate_user_config` takes a JSON argument describing the node and
writes a ready-to-run config file to the `output` path, plus a sibling
`keystore.yaml` holding freshly-generated default keys — no node or
network required. We write the config to `./user-config.yaml`; a `0`
result means success. The JSON is passed with logoscore's `@file` syntax
after writing it to disk:
file:
path: gen-config.json
content: |
{
"output": "./user-config.yaml"
}
run: "./logos/bin/logoscore call liblogos_blockchain_module generate_user_config @gen-config.json"
code_block: "logoscore call liblogos_blockchain_module generate_user_config @gen-config.json"
expect_contains:
- '"result":0'
check_file: "user-config.yaml"
- title: "Confirm the keystore was written alongside the config"
text: |
`generate_user_config` also writes a `keystore.yaml` next to the config,
holding the node's freshly-generated default keys. Both files are written
relative to the daemon's working directory:
run: "ls -1 user-config.yaml keystore.yaml"
check_file: "keystore.yaml"
- title: "Derive the node's peer id"
text: |
`get_peer_id` reads the network key out of the config we just generated
and derives the libp2p peer id from it — a deterministic, offline
round-trip through the logos-blockchain C library. The result is the
node's base58 peer id (the `12D3Koo…` form):
run: "./logos/bin/logoscore call liblogos_blockchain_module get_peer_id ./user-config.yaml"
code_block: "logoscore call liblogos_blockchain_module get_peer_id ./user-config.yaml"
expect_contains:
- '"result":"12D3Koo'
- title: "Update the user-config from the keystore"
text: |
`update_user_config` re-syncs a user config with the keys in a keystore
file — the same offline maintenance operation the `update-config` CLI
command performs. It takes the config path and the keystore path and
returns `0` on success. We point it at the pair generated above:
run: "./logos/bin/logoscore call liblogos_blockchain_module update_user_config ./user-config.yaml ./keystore.yaml"
code_block: "logoscore call liblogos_blockchain_module update_user_config ./user-config.yaml ./keystore.yaml"
expect_contains:
- '"result":0'
- title: "Query consensus info before the node is running"
text: |
The node-backed methods (wallet, explorer, consensus) need a started
node. Calling `get_cryptarchia_info` now — before `start` — returns the
module's well-defined `The node is not running` message. This proves the
method is wired through the IPC bridge and callable; actually starting
the node (which dials the devnet) is out of scope for this offline
doc-test:
run: "./logos/bin/logoscore call liblogos_blockchain_module get_cryptarchia_info"
code_block: "logoscore call liblogos_blockchain_module get_cryptarchia_info"
expect_contains:
- "The node is not running"
- title: "Query a wallet balance before the node is running"
text: |
`wallet_get_balance` behaves the same way — it reports the node is not
running rather than crashing, so a frontend can surface a clean error:
run: "./logos/bin/logoscore call liblogos_blockchain_module wallet_get_balance aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
code_block: "logoscore call liblogos_blockchain_module wallet_get_balance <address-hex>"
expect_contains:
- "The node is not running"
- title: "Query the block explorer before the node is running"
text: |
The explorer method `get_block` is the same: callable through the bridge,
and reporting the node is not running until one is started:
run: "./logos/bin/logoscore call liblogos_blockchain_module get_block aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
code_block: "logoscore call liblogos_blockchain_module get_block <header-id-hex>"
expect_contains:
- "The node is not running"
- title: "Stop the daemon"
text: "Shut the daemon down cleanly:"
run: "./logos/bin/logoscore stop"
code_block: "logoscore stop"
post_text: |
The daemon removes its state file and exits.
- run: "sleep 2"
- title: "Confirm the daemon has stopped"
text: |
With no daemon running, the client reports `not_running` and exits
non-zero, so we add `|| true` to let the doc-test assert on the output:
run: "./logos/bin/logoscore status || true"
code_block: "logoscore status"
expect_contains:
- '"status":"not_running"'

View File

@ -0,0 +1,338 @@
# Running This Blockchain Module Against logoscore
`logos-blockchain-module` is a Logos `core` module that wraps the
[logos-blockchain](https://github.com/logos-blockchain/logos-blockchain) C
bindings — a full Cryptarchia consensus node with wallet, blend, keystore and
block-explorer APIs — and ships the zk circuit binaries the node needs at
runtime. This doc-test exercises **this** blockchain-module commit end-to-end
through the headless `logoscore` runtime:
1. Build the `logoscore` CLI and the `lgpm` local package manager from their
published flakes. `logoscore` is the headless frontend for `logos-liblogos`,
so building it brings in the whole module-runtime stack (`logos_host`,
`liblogos_core`, the IPC layer).
2. Build **this** blockchain module as an installable `.lgx` package straight
from its own flake's `#lgx` output, **pinned to the commit under test** — so
the module you run is built from exactly what is checked out here, not the
latest published release.
3. Install the `.lgx` into a `./modules` directory with `lgpm`.
4. Start `logoscore` in daemon mode (`-D`), load `liblogos_blockchain_module`, introspect
it with `module-info`, and call several of its methods — verifying the module
actually runs and round-trips real values through the logos-blockchain C
library.
Joining the devnet means dialing real peers, NTP sync and zk proving — none of
which is reproducible in CI — so this doc-test deliberately stays **offline**.
We exercise the methods that do real, deterministic work without a running node
(generating a node user-config and its keystore, deriving the peer id,
re-syncing the config from the keystore) and we confirm the node-backed methods
(wallet, explorer, consensus) are wired up and callable by asserting on the
well-defined `The node is not running` response they return before a node is
started. Starting an actual node is covered by the UI app and the developer
guide.
Because the module is built from the commit under test and then loaded and
called through a real `logoscore` daemon, a green run is real evidence that this
change keeps the blockchain module loadable and callable.
**What you'll build:** This `liblogos_blockchain_module`, packaged as `.lgx`, installed with `lgpm`, and called through a `logoscore` daemon.
**What you'll learn:**
- How to build the `logoscore` runtime and the `lgpm` package manager from their flakes
- How a module's flake exposes a ready-to-install `.lgx` via its `#lgx` output
- How to install an `.lgx` into a modules directory with `lgpm`
- How to start the `logoscore` daemon, load a module, introspect it, and call its methods
- How to generate a node user-config and keystore, and derive the peer id, offline
- How to re-sync a user config from its keystore with `update_user_config`
- How the node-backed methods report a clear error until a node is started
- How to shut the daemon down and confirm it has exited
## Prerequisites
- **Nix** with flakes enabled. Install from [nixos.org](https://nixos.org/download.html), then enable flakes:
```bash
mkdir -p ~/.config/nix
echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
```
Verify: `nix flake --help >/dev/null 2>&1 && echo "Flakes enabled"`
- **A Linux or macOS machine.**
---
## Step 1: Build logoscore
Build the `logoscore` CLI from its published flake. The result is symlinked to
`./logos/`. `logoscore` is the headless frontend for `logos-liblogos`, so this
one build brings in the whole module-runtime stack the daemon needs.
### 1.1 Build the CLI
```bash
nix build 'github:logos-co/logos-logoscore-cli' --out-link ./logos
```
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).
---
## Step 2: Build the lgpm package manager
`lgpm` installs `.lgx` packages into a modules directory and scans what is
installed. Build it from the `logos-package-manager` flake and link it as
`./lgpm`.
### 2.1 Build lgpm
```bash
nix build 'github:logos-co/logos-package-manager#cli' -o lgpm
```
The executable is at `./lgpm/bin/lgpm`.
---
## Step 3: Build and install this blockchain module
Build **this** blockchain module's `.lgx` straight from its flake's `#lgx`
output and install it into a local `./modules` directory with `lgpm`. Every
module built with
[`logos-module-builder`](https://github.com/logos-co/logos-module-builder)
exposes a ready-to-install `#lgx`.
> The `` in the URL is what pins the build to a specific commit: the
> doc-test runner expands it to a concrete ref. Locally that is this
> checkout's `HEAD` (see `run.sh`); in CI it is the commit being tested. With
> no pin it falls back to the latest `master`.
### 3.1 Build the module's .lgx
Build the `#lgx` output and link it as `./blockchain-lgx`. (This compiles
the module, the logos-blockchain Rust node and its zk circuits through
Nix, so the first build is slow.)
```bash
# From inside the clone this is simply: nix build '.#lgx'
nix build 'github:logos-blockchain/logos-blockchain-module#lgx' -o blockchain-lgx
```
The `.lgx` package is now under `./blockchain-lgx/`:
```bash
ls blockchain-lgx/*.lgx
```
### 3.2 Seed the modules directory with the bundled capability module
`liblogos_blockchain_module` is loaded through the host's capability layer, so the
modules directory also needs the `capability_module` that ships with
`logoscore`. Copy it across first.
```bash
mkdir -p modules
cp -RL ./logos/modules/. ./modules/
```
### 3.3 Install the .lgx with lgpm
Install the freshly-built package into `./modules`. `liblogos_blockchain_module` is
a `core` module, so it goes to `--modules-dir`. The package is unsigned
(a local dev build), so we pass `--allow-unsigned`.
```bash
./lgpm/bin/lgpm --modules-dir ./modules --allow-unsigned install --file blockchain-lgx/*.lgx
```
### 3.4 Confirm the install
Scan the directory and confirm the module landed:
```bash
./lgpm/bin/lgpm --modules-dir ./modules list
```
---
## Step 4: Run the daemon and call the module
Start `logoscore` in daemon mode pointed at `./modules`, then use the client
subcommands to load `liblogos_blockchain_module`, introspect it, and call several of
its methods. Daemon output is captured in `logs.txt`.
### 4.1 Start the daemon
Start logoscore in daemon mode in the background, capturing output to
`logs.txt`:
```bash
logoscore -D -m ./modules > logs.txt &
```
The `-D` flag starts the daemon. The client subcommands below connect to
this running process via the config written under `~/.logoscore/`.
```bash
sleep 3
```
### 4.2 Inspect the startup log
Review the daemon's startup output:
```bash
cat logs.txt
```
### 4.3 Check daemon status
Verify the daemon is running:
```bash
logoscore status
```
### 4.4 List discovered modules
`liblogos_blockchain_module` should be visible in the scan directory:
```bash
logoscore list-modules
```
### 4.5 Load the module
Load `liblogos_blockchain_module` into the running daemon:
```bash
logoscore load-module liblogos_blockchain_module
```
### 4.6 Confirm the module is loaded
Re-run `status`; the module that was `not_loaded` before now reports
`loaded`:
```bash
logoscore status
```
### 4.7 Introspect the module with module-info
`module-info` lists the `Q_INVOKABLE` methods the module exposes — the
same methods you can `call`:
```bash
logoscore module-info liblogos_blockchain_module
```
### 4.8 Generate a node user-config
`generate_user_config` takes a JSON argument describing the node and
writes a ready-to-run config file to the `output` path, plus a sibling
`keystore.yaml` holding freshly-generated default keys — no node or
network required. We write the config to `./user-config.yaml`; a `0`
result means success. The JSON is passed with logoscore's `@file` syntax
after writing it to disk:
```json
{
"output": "./user-config.yaml"
}
```
```bash
logoscore call liblogos_blockchain_module generate_user_config @gen-config.json
```
### 4.9 Confirm the keystore was written alongside the config
`generate_user_config` also writes a `keystore.yaml` next to the config,
holding the node's freshly-generated default keys. Both files are written
relative to the daemon's working directory:
```bash
ls -1 user-config.yaml keystore.yaml
```
### 4.10 Derive the node's peer id
`get_peer_id` reads the network key out of the config we just generated
and derives the libp2p peer id from it — a deterministic, offline
round-trip through the logos-blockchain C library. The result is the
node's base58 peer id (the `12D3Koo…` form):
```bash
logoscore call liblogos_blockchain_module get_peer_id ./user-config.yaml
```
### 4.11 Update the user-config from the keystore
`update_user_config` re-syncs a user config with the keys in a keystore
file — the same offline maintenance operation the `update-config` CLI
command performs. It takes the config path and the keystore path and
returns `0` on success. We point it at the pair generated above:
```bash
logoscore call liblogos_blockchain_module update_user_config ./user-config.yaml ./keystore.yaml
```
### 4.12 Query consensus info before the node is running
The node-backed methods (wallet, explorer, consensus) need a started
node. Calling `get_cryptarchia_info` now — before `start` — returns the
module's well-defined `The node is not running` message. This proves the
method is wired through the IPC bridge and callable; actually starting
the node (which dials the devnet) is out of scope for this offline
doc-test:
```bash
logoscore call liblogos_blockchain_module get_cryptarchia_info
```
### 4.13 Query a wallet balance before the node is running
`wallet_get_balance` behaves the same way — it reports the node is not
running rather than crashing, so a frontend can surface a clean error:
```bash
logoscore call liblogos_blockchain_module wallet_get_balance <address-hex>
```
### 4.14 Query the block explorer before the node is running
The explorer method `get_block` is the same: callable through the bridge,
and reporting the node is not running until one is started:
```bash
logoscore call liblogos_blockchain_module get_block <header-id-hex>
```
### 4.15 Stop the daemon
Shut the daemon down cleanly:
```bash
logoscore stop
```
The daemon removes its state file and exits.
```bash
sleep 2
```
### 4.16 Confirm the daemon has stopped
With no daemon running, the client reports `not_running` and exits
non-zero, so we add `|| true` to let the doc-test assert on the output:
```bash
logoscore status
```

76
doctests/run.sh Normal file
View File

@ -0,0 +1,76 @@
#!/usr/bin/env bash
#
# Execute the blockchain-module doc-test end-to-end and regenerate its Markdown.
#
# There is one spec:
# blockchain-module-runtime.test.yaml — packages this module as an .lgx,
# installs it with lgpm, and drives it through a headless logoscore daemon.
#
# The runner is the shared `doctest` CLI
# (https://github.com/logos-co/logos-doctest), invoked directly via its flake.
# Each spec runs into ./outputs/ via --output-dir; `doctest generate` renders the
# .md; `doctest clean` then strips build artifacts, keeping only the .md.
#
# To run against a local logos-doctest checkout instead of the published flake,
# set DOCTEST, e.g.: DOCTEST="nix run path:../../logos-doctest --" ./run.sh
#
set -euo pipefail
# Run from this doctests/ directory regardless of where the script is invoked from.
cd "$(dirname "$0")"
# The doctest CLI. Override by exporting DOCTEST (space-separated command).
read -r -a DOCTEST <<< "${DOCTEST:-nix run github:logos-co/logos-doctest --}"
OUTPUT_DIR="./outputs"
# Build the doc-tests against THIS repo's current commit rather than the latest
# published flake. The spec pins `github:logos-blockchain/logos-blockchain-module{release}`
# to $COMMIT via --release-for, so the runtime spec packages exactly what's
# checked out here. Override by exporting COMMIT (e.g. a tag), or set COMMIT="" to
# fall back to latest master.
#
# Note: nix fetches the commit from the GitHub remote, so $COMMIT must be pushed
# to logos-blockchain/logos-blockchain-module. A local-only / uncommitted HEAD
# won't resolve; export COMMIT="" (or push first) in that case.
COMMIT="${COMMIT-$(git rev-parse HEAD)}"
RELEASE_FOR=()
if [ -n "${COMMIT}" ]; then
RELEASE_FOR=(--release-for "logos-blockchain-module=${COMMIT}")
echo "==> Pinning logos-blockchain-module to ${COMMIT}"
else
echo "==> COMMIT empty; building from latest logos-blockchain-module master"
fi
echo "==> Clearing previous ${OUTPUT_DIR}/"
# A prior run copies module artifacts out of the read-only nix store, so the
# directories land read-only (r-x) too. `rm -rf` can't delete files inside a
# directory it can't write to, so restore write permission first.
if [ -e "${OUTPUT_DIR}" ]; then
chmod -R u+w "${OUTPUT_DIR}" 2>/dev/null || true
fi
rm -rf "${OUTPUT_DIR}"
mkdir -p "${OUTPUT_DIR}"
# Run each spec into ./outputs/ separately. --output-dir is single-spec, but
# passing it once per spec keeps each spec's artifacts beside its generated .md.
for spec in *.test.yaml; do
name="$(basename "${spec%.test.yaml}")"
echo "==> Running ${spec} into ${OUTPUT_DIR}/"
# ${RELEASE_FOR[@]+...} guards the expansion so an empty array doesn't trip
# `set -u` on older bash (e.g. macOS's stock 3.2).
"${DOCTEST[@]}" run "${spec}" \
--verbose \
--continue-on-fail \
${RELEASE_FOR[@]+"${RELEASE_FOR[@]}"} \
--output-dir "${OUTPUT_DIR}/"
echo "==> Generating ${OUTPUT_DIR}/${name}.md"
"${DOCTEST[@]}" generate "${spec}" \
${RELEASE_FOR[@]+"${RELEASE_FOR[@]}"} \
-o "${OUTPUT_DIR}/${name}.md"
done
echo "==> Cleaning build artifacts from ${OUTPUT_DIR}/ (keeps .md)"
"${DOCTEST[@]}" clean "${OUTPUT_DIR}" --verbose
echo "==> Done. Rendered docs are in ${OUTPUT_DIR}/"