-
Notifications
You must be signed in to change notification settings - Fork 182
Add forest api test-stateful subcommand
#5836
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughAdds a stateful RPC test harness and CLI subcommand to run Ethereum filter-related integration tests against a live node, plus docs, ERC‑20 test assets, CI job and runner script, a WebSocket dependency, and small RPC/type/serialization and visibility adjustments. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
rect rgba(240,248,255,0.9)
participant CLI as User (forest-tool)
participant TH as Test Harness
participant RPC as rpc::Client (HTTP + WS)
participant N as Forest Node
end
CLI->>TH: invoke test-stateful (to, from, payload, topic, filter)
TH->>RPC: build client (HTTP + WS)
TH->>TH: create_tests(TestTransaction)
loop per scenario
alt subscription-based
TH->>RPC: subscribe via WS (heads/pending)
RPC-->>TH: subscription notifications
end
TH->>RPC: eth_newFilter / eth_newBlockFilter / eth_newPendingTransactionFilter
TH->>N: trigger event (invoke contract / send tx)
N-->>RPC: emit logs / heads / pending
TH->>RPC: eth_getFilterChanges / eth_getFilterLogs
RPC-->>TH: return Hashes / Logs
TH-->>CLI: scenario pass/fail
end
TH-->>CLI: aggregated summary
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested reviewers
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (2)
src/tool/subcommands/api_cmd.rs (2)
231-246: Optional: parse strongly-typed args at the clap boundary to centralize validationLet clap parse Address/EthHash/hex bytes for you to reduce error-prone String plumbing and push validation to the boundary. This keeps run() lean and avoids duplicated parsing logic.
Minimal change (no external deps), using a small parser for hex:
- /// Test Transaction `to` address - #[arg(long)] - to: String, - /// Test Transaction `from` address - #[arg(long)] - from: String, - /// Test Transaction hex `payload` - #[arg(long)] - payload: String, - /// Log `topic` to search for - #[arg(long)] - topic: String, + /// Test Transaction `to` address + #[arg(long, value_parser = clap::builder::ValueParser::new(Address::from_str))] + to: Address, + /// Test Transaction `from` address + #[arg(long, value_parser = clap::builder::ValueParser::new(Address::from_str))] + from: Address, + /// Test Transaction hex `payload` (accepts optional 0x prefix) + #[arg(long, value_parser = parse_hex_bytes)] + payload: Vec<u8>, + /// Log `topic` to search for (0x‑prefixed 32‑byte hash) + #[arg(long, value_parser = clap::builder::ValueParser::new(EthHash::from_str))] + topic: EthHash,Add this helper in the same file (top-level):
fn parse_hex_bytes(s: &str) -> Result<Vec<u8>, String> { let clean = s.strip_prefix("0x").unwrap_or(s); hex::decode(clean).map_err(|e| format!("invalid hex: {s} ({e})")) }With this, the run() arm can drop all manual parsing lines and directly construct TestTransaction.
395-417: Accept “0x”-prefixed payloads and add parsing context to improve UXhex::decode(payload) will reject the commonly used 0x-prefixed form; address/topic errors also lack context. Strip an optional 0x and wrap parses with with_context. Also trim filter to avoid surprises from trailing spaces.
} => { - let client = Arc::new(rpc::Client::default_or_from_env(None)?); + let client = Arc::new( + rpc::Client::default_or_from_env(None) + .with_context(|| "could not initialize RPC client; check FULLNODE_API_INFO or that a node is running on 127.0.0.1:2345")? + ); - let to = Address::from_str(&to)?; - let from = Address::from_str(&from)?; - let payload = hex::decode(payload)?; - let topic = EthHash::from_str(&topic)?; + let to = Address::from_str(&to) + .with_context(|| format!("invalid --to address: {to}"))?; + let from = Address::from_str(&from) + .with_context(|| format!("invalid --from address: {from}"))?; + let payload = { + let clean = payload.strip_prefix("0x").unwrap_or(&payload); + hex::decode(clean) + .with_context(|| format!("invalid --payload hex: {payload}"))? + }; + let topic = EthHash::from_str(&topic) + .with_context(|| format!("invalid --topic (expect 0x-prefixed 32-byte hash): {topic}"))?; let tx = TestTransaction { to, from, payload, topic, }; - let tests = stateful_tests::create_tests(tx).await; - stateful_tests::run_tests(tests, client, filter).await?; + let tests = stateful_tests::create_tests(tx).await; + let filter = filter.trim().to_string(); + stateful_tests::run_tests(tests, client, filter).await?; }
🧹 Nitpick comments (1)
src/tool/subcommands/api_cmd.rs (1)
208-246: Tighten CLI help: document address/hex formats, FULLNODE_API_INFO, and filter semanticsCurrent help omits crucial format guidance and environment usage. Clarify that:
- to/from must be Filecoin addresses (f/t or delegated f4).
- payload is hex and may include an optional 0x prefix.
- topic is a 32‑byte Keccak hash (0x… with 64 hex nibbles).
- connection is taken from FULLNODE_API_INFO (or defaults to 127.0.0.1:2345).
- filter is case‑sensitive and matches by method-name prefix.
Apply this doc-only diff:
- /// Run multiple stateful JSON-RPC API tests against a Filecoin node. + /// Run a suite of stateful JSON-RPC API tests against a Filecoin node. /// - /// Some tests require sending a transaction to trigger events; the provided - /// `from`, `to`, `payload`, and `topic` inputs are used for those cases. + /// Connection: + /// - Uses `FULLNODE_API_INFO` if set (e.g. `/ip4/127.0.0.1/tcp/2345/http` or a multiaddr with token). + /// Falls back to `http://127.0.0.1:2345/`. + /// + /// Inputs: + /// - `--from`, `--to`: Filecoin addresses (`f…`/`t…`, including delegated `f4`). + /// - `--payload`: hex data; both `deadbeef` and `0xdeadbeef` are accepted. + /// - `--topic`: 32-byte event topic (Keccak-256), 0x-prefixed 64-nybble hex. /// - /// Useful for verifying methods like `eth_newFilter`, `eth_getFilterLogs`, and others - /// that rely on internal state. + /// Useful for verifying methods like `eth_newFilter`, `eth_getFilterLogs`, and others that rely on internal state. /// - /// Use `--filter` to run only tests that interact with a specific RPC method. + /// Use `--filter` to run only tests that interact with a specific RPC method. + /// The match is case-sensitive and by method-name prefix (e.g. `--filter eth_new`). /// - /// Example output: - /// ```text + /// Example (Lotus; all pass): + /// ```console /// running 7 tests /// test eth_newFilter install/uninstall ... ok /// test eth_newFilter under limit ... ok /// test eth_newFilter just under limit ... ok /// test eth_newFilter over limit ... ok /// test eth_newBlockFilter works ... ok /// test eth_newPendingTransactionFilter works ... ok /// test eth_getFilterLogs works ... ok /// test result: ok. 7 passed; 0 failed; 0 ignored; 0 filtered out /// ``` + /// Example (Forest; some tests currently ignored/pending fixes): + /// ```console + /// running 7 tests + /// test eth_newFilter install/uninstall ... ok + /// test eth_newFilter under limit ... ok + /// test eth_newFilter just under limit ... ok + /// test eth_newFilter over limit ... ignored + /// test eth_newBlockFilter works ... ok + /// test eth_newPendingTransactionFilter works ... ignored + /// test eth_getFilterLogs works ... ignored + /// test result: ok. 4 passed; 0 failed; 3 ignored; 0 filtered out + /// ```
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
scripts/tests/calibnet_api_test_stateful_check.sh(1 hunks)src/tool/subcommands/api_cmd.rs(5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- scripts/tests/calibnet_api_test_stateful_check.sh
🧰 Additional context used
🧬 Code graph analysis (1)
src/tool/subcommands/api_cmd.rs (4)
src/rpc/client.rs (2)
client(114-114)default_or_from_env(46-77)src/rpc/methods/eth.rs (13)
new(512-524)new(677-682)from_str(262-264)from_str(370-381)from_str(430-449)from(136-138)from(142-144)from(268-271)from(275-277)filter(3101-3101)filter(3151-3151)filter(3176-3176)filter(3208-3208)src/shim/address.rs (2)
from_str(178-183)from_str(299-302)src/tool/subcommands/api_cmd/stateful_tests.rs (2)
create_tests(606-655)run_tests(91-168)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
- GitHub Check: tests-release
- GitHub Check: tests
- GitHub Check: Build forest binaries on Linux AMD64
- GitHub Check: All lint checks
- GitHub Check: cargo-publish-dry-run
- GitHub Check: Build MacOS
- GitHub Check: Build Ubuntu
- GitHub Check: Analyze (go)
- GitHub Check: Analyze (rust)
🔇 Additional comments (3)
src/tool/subcommands/api_cmd.rs (3)
7-7: Adding stateful_tests module — LGTMModule layout looks consistent with the rest of the file’s submodules.
21-21: Importing TestTransaction — LGTMDirect import keeps the call site in run() clean.
30-30: FromStr import — LGTMNeeded for Address/EthHash parsing below.
|
no green checkmark! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (2)
src/tool/subcommands/api_cmd.rs (2)
236-252: Prefer strong typing at the clap boundary for--to,--from,--topicLet clap parse
AddressandEthHashviaFromStrto fail fast with better help and remove ad-hoc parsing later. Keeppayloadas a string (we still need custom 0x handling). This mirrors prior feedback.Apply this refactor (fields only):
- /// Test Transaction `to` address - #[arg(long)] - to: String, - /// Test Transaction `from` address - #[arg(long)] - from: String, + /// Test Transaction `to` address (delegated f4) + #[arg(long)] + to: Address, + /// Test Transaction `from` address (delegated f4) + #[arg(long)] + from: Address, @@ - /// Log `topic` to search for - #[arg(long)] - topic: String, + /// Log `topic` (32‑byte Keccak hash) to search for + #[arg(long)] + topic: EthHash,If you adopt this, the manual
Address::from_str/EthHash::from_strcalls in the match arm can be dropped (see follow-up diff in Lines 401‑423). Also, you can remove the unuseduse std::str::FromStr;import (Line 30).
401-423: Add user-friendly parse context, accept 0x‑prefixed payloads, and improve FULLNODE_API_INFO error
- Wrap
default_or_from_envwith context to guide users when the env var is missing/invalid.- Accept “0x…” payloads by stripping the prefix before
hex::decode.- Add
with_contexton parse errors for clearer CLI UX.- Pass
clientdirectly (already done; no clone needed).Minimal, backward-compatible patch:
} => { - let client = Arc::new(rpc::Client::default_or_from_env(None)?); + let client = Arc::new( + rpc::Client::default_or_from_env(None) + .with_context(|| "failed to initialize RPC client (set FULLNODE_API_INFO or ensure a node is listening on http://127.0.0.1:2345/)")? + ); - let to = Address::from_str(&to)?; - let from = Address::from_str(&from)?; - let payload = hex::decode(payload)?; - let topic = EthHash::from_str(&topic)?; + let to = Address::from_str(&to) + .with_context(|| format!("invalid --to address: {to}"))?; + let from = Address::from_str(&from) + .with_context(|| format!("invalid --from address: {from}"))?; + let payload = { + let clean = payload.strip_prefix("0x").unwrap_or(&payload); + hex::decode(clean) + .with_context(|| format!("invalid --payload hex: {payload}"))? + }; + let topic = EthHash::from_str(&topic) + .with_context(|| format!("invalid --topic (expect 0x‑prefixed 32‑byte hash): {topic}"))?; let tx = TestTransaction { to, from, payload, topic, }; let tests = stateful_tests::create_tests(tx).await; stateful_tests::run_tests(tests, client, filter).await?; }If you adopt typed clap fields (Lines 236‑252), simplify this arm further:
- let to = Address::from_str(&to) - .with_context(|| format!("invalid --to address: {to}"))?; - let from = Address::from_str(&from) - .with_context(|| format!("invalid --from address: {from}"))?; - let payload = { + let payload = { let clean = payload.strip_prefix("0x").unwrap_or(&payload); hex::decode(clean) .with_context(|| format!("invalid --payload hex: {payload}"))? }; - let topic = EthHash::from_str(&topic) - .with_context(|| format!("invalid --topic (expect 0x‑prefixed 32‑byte hash): {topic}"))?; let tx = TestTransaction { - to, - from, + to, // already Address + from, // already Address payload, - topic, + topic, // already EthHash };
🧹 Nitpick comments (3)
src/tool/subcommands/api_cmd.rs (3)
208-235: Tighten docs: clarify connection fallback, input formats, and filter semantics
- Mention the default fallback when FULLNODE_API_INFO is not set (http://127.0.0.1:2345/).
- Call out that hex inputs commonly include a “0x” prefix and whether it’s accepted.
- Clarify that
--filterdoes a prefix match (starts_with), not substring.- The “all ok (7 passed)” example doesn’t reflect current ignores/expected failures; either add a disclaimer or show a realistic mixed/ignored output.
Apply this doc-only diff:
- /// Connection: uses `FULLNODE_API_INFO` from the environment. + /// Connection: uses `FULLNODE_API_INFO` from the environment; falls back to http://127.0.0.1:2345/. @@ - /// - `--payload`: calldata in hex - /// - `--topic`: `32‑byte` event topic in hex - /// - `--filter`: run only tests that interact with a specific RPC method + /// - `--payload`: calldata in hex (accept “0x” prefix or raw hex) + /// - `--topic`: 32‑byte event topic in hex (e.g., 0x…) + /// - `--filter`: run only tests whose method name starts with this string (case sensitive) @@ - /// Example output: + /// Example output (results may include ignored or expected failures, depending on node): @@ - /// test eth_newFilter over limit ... ok + /// test eth_newFilter over limit ... ignored @@ - /// test eth_newPendingTransactionFilter works ... ok - /// test eth_getFilterLogs works ... ok - /// test result: ok. 7 passed; 0 failed; 0 ignored; 0 filtered out + /// test eth_newPendingTransactionFilter works ... ignored + /// test eth_getFilterLogs works ... ignored + /// test result: ok. 4 passed; 0 failed; 3 ignored; 0 filtered out
30-30:use std::str::FromStr;likely unnecessary if clap parses typed argsIf you move to typed clap fields for
to,from, andtopic, this import can be removed.-use std::str::FromStr;
208-252: Optional: makepayloadoptional to ease running filter-only testsRight now users must provide
--payloadeven when running, e.g.,--filter EthNewBlockFilter, which doesn’t require tx data. ConsiderOption<String>and validate only when a test needs it. Happy to draft if desired.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
.config/forest.dic(2 hunks)src/tool/subcommands/api_cmd.rs(5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/tool/subcommands/api_cmd.rs (4)
src/rpc/client.rs (2)
client(114-114)default_or_from_env(46-77)src/rpc/methods/eth.rs (13)
new(512-524)new(677-682)from_str(262-264)from_str(370-381)from_str(430-449)from(136-138)from(142-144)from(268-271)from(275-277)filter(3101-3101)filter(3151-3151)filter(3176-3176)filter(3208-3208)src/shim/address.rs (2)
from_str(178-183)from_str(299-302)src/tool/subcommands/api_cmd/stateful_tests.rs (2)
create_tests(606-655)run_tests(91-168)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
- GitHub Check: Build forest binaries on Linux AMD64
- GitHub Check: tests
- GitHub Check: tests-release
- GitHub Check: All lint checks
- GitHub Check: cargo-publish-dry-run
- GitHub Check: Build MacOS
- GitHub Check: Build Ubuntu
- GitHub Check: Analyze (rust)
- GitHub Check: Analyze (go)
🔇 Additional comments (4)
.config/forest.dic (2)
19-19: Add “calldata” to dictionary — LGTMCorrect Ethereum term; avoids false-positive spellcheck hits in docs and code comments.
55-55: Add “f4” to dictionary — LGTMMatches delegated Filecoin address prefix used in the new CLI docs.
src/tool/subcommands/api_cmd.rs (2)
7-7: Newstateful_testsmodule inclusion — LGTMModularizing the stateful suite keeps
api_cmdtidy.
21-21: Import ofTestTransaction— LGTMNarrow import is clear and localizes the test harness dependency.
|
the stateful check is failing! no review! Let's ensure it's not flaky. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (2)
scripts/tests/harness.sh (2)
127-135: Backfill path should wait for full healthcheck readiness to avoid flaky stateful testsPer team learnings, when backfilling you should wait for “healthcheck ready” (not just API up/sync) before running tests that depend on completed DB backfill.
Add this guard near the end of forest_init after sync (outside the changed hunk, shown here as a plain snippet):
# After forest_wait_for_sync if [[ "${1:-}" == "--backfill-db" ]]; then forest_wait_for_healthcheck_ready fi
143-147: Fail fast on missing token, trim newline, and avoid overriding FULLNODE_API_INFO unless necessaryRight now, if the token file is missing/empty, you’ll silently export an auth-less FULLNODE_API_INFO and the client will not fall back to the default token loader, causing 401s. Also, most token files end with a newline, which will break auth unless trimmed.
Apply this diff to make the block robust and cwd-agnostic while keeping the env override opt-in:
- DATA_DIR=$( $FOREST_CLI_PATH config dump | grep "data_dir" | cut -d' ' -f3- | tr -d '"' ) - ADMIN_TOKEN=$(cat "${DATA_DIR}/token") - FULLNODE_API_INFO="${ADMIN_TOKEN}:/ip4/127.0.0.1/tcp/2345/http" - - export FULLNODE_API_INFO + # Prefer default token path resolution in forest-tool; only export FULLNODE_API_INFO if not already provided. + if [[ -z "${FULLNODE_API_INFO:-}" ]]; then + DATA_DIR=$($FOREST_CLI_PATH config dump | grep "data_dir" | cut -d' ' -f3- | tr -d '"') + TOKEN_FILE="${DATA_DIR}/token" + if [[ ! -s "$TOKEN_FILE" ]]; then + echo "ERROR: RPC token file missing or empty at: $TOKEN_FILE" >&2 + return 1 + fi + # Trim trailing newline to avoid an invalid password + ADMIN_TOKEN="$(tr -d '\n' < "$TOKEN_FILE")" + export FULLNODE_API_INFO="${ADMIN_TOKEN}:/ip4/127.0.0.1/tcp/2345/http" + fi
🧹 Nitpick comments (1)
src/tool/subcommands/api_cmd.rs (1)
207-251: Tighten docs: connection fallback and filter semantics
- Clarify that connection falls back to 127.0.0.1:2345 + default token file when FULLNODE_API_INFO is not set (matches rpc::Client::default_or_from_env).
- Be explicit that filter uses a case-sensitive prefix match (matches run_tests).
Apply this doc tweak:
- /// Connection: uses `FULLNODE_API_INFO` from the environment. + /// Connection: uses `FULLNODE_API_INFO` from the environment, or falls back to + /// `http://127.0.0.1:2345` with the default token file if unset. @@ - /// - `--topic`: `32‑byte` event topic in hex - /// - `--filter`: run only tests that interact with a specific RPC method + /// - `--topic`: `32‑byte` event topic in hex (expects 0x‑prefixed) + /// - `--filter`: run only tests whose method name starts with this string (prefix, case sensitive)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
scripts/tests/calibnet_api_test_stateful_check.sh(1 hunks)scripts/tests/harness.sh(1 hunks)src/tool/subcommands/api_cmd.rs(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- scripts/tests/calibnet_api_test_stateful_check.sh
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-07T09:37:03.079Z
Learnt from: hanabi1224
PR: ChainSafe/forest#5886
File: scripts/tests/calibnet_eth_mapping_check.sh:49-49
Timestamp: 2025-08-07T09:37:03.079Z
Learning: In Forest project scripts, `forest_wait_api` (called within `forest_init`) ensures basic RPC service readiness, while `forest_wait_for_healthcheck_ready` performs additional comprehensive checks including DB backfill completion. When using `--backfill-db` flag, basic RPC operations can proceed after `forest_init`, but operations requiring complete DB backfill should wait for `forest_wait_for_healthcheck_ready`.
Applied to files:
scripts/tests/harness.sh
🧬 Code graph analysis (1)
src/tool/subcommands/api_cmd.rs (2)
src/rpc/client.rs (2)
client(114-114)default_or_from_env(46-77)src/tool/subcommands/api_cmd/stateful_tests.rs (2)
create_tests(606-655)run_tests(91-168)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: tests
- GitHub Check: tests-release
- GitHub Check: Build forest binaries on Linux AMD64
🔇 Additional comments (2)
src/tool/subcommands/api_cmd.rs (2)
7-7: Module wiring LGTMThe new stateful_tests module and TestTransaction import are cleanly integrated.
Also applies to: 21-21
400-423: Add context to RPC client construction errorThe suggested minimal change enriches the error message when constructing the RPC client, guiding users to check their environment variables and node connection.
• File – src/tool/subcommands/api_cmd.rs
• Section – theSelf::TestStatefularm around line 410Apply this diff:
- let client = Arc::new(rpc::Client::default_or_from_env(None)?); + let client = Arc::new( + rpc::Client::default_or_from_env(None) + .context("failed to construct RPC client: ensure FULLNODE_API_INFO is valid, the default token file is accessible, and a node is running at 127.0.0.1:2345")? + );Optional follow-up: if the stateful tests require only Delegated addresses, validate the transaction’s
fromandtoprotocols viaAddress::protocol()and return an early error. SinceProtocolis re-exported from the externalfvm_shared4crate, please verify it includes aDelegatedvariant before implementing this check.
Summary of changes
Changes introduced in this pull request:
api test-statefulsubcommand toforest-toolUsage example:
Lotus:
Forest:
Reference issue to close (if applicable)
Closes #5793, #5975
Other information and links
Change checklist
Summary by CodeRabbit
New Features
Documentation
Refactor
Bug Fixes
Chores
Improvements