15 KiB
Logseq CLI and db-worker-node Async Test Isolation Plan
Goal: Audit all CLI-related async test cases and ensure every mocked function, overridden global binding, and shared test state is reset after each test case so no test leaks state into later tests.
Architecture: Prefer per-test scoped mocking (with-redefs / p/with-redefs) and explicit :each fixtures for mutable globals, while keeping real process/server lifecycle cleanup in p/finally so async resources are always released.
Tech Stack: ClojureScript, cljs.test, promesa, Node.js-based test runtime.
Related: Builds on docs/agent-guide/001-logseq-cli.md, docs/agent-guide/003-db-worker-node-cli-orchestration.md, docs/agent-guide/015-logseq-cli-db-worker-node-housekeeping.md, and docs/agent-guide/035-logseq-cli-db-worker-deps-cli-decoupling.md.
Problem statement
Current CLI-related async tests rely heavily on mutating namespace vars and process globals with set!, then restoring them manually in p/finally blocks.
That pattern works only if every async branch reaches cleanup. It is easy to miss one restore path, and a failed or aborted test can leave mutated globals behind for later tests.
The most leak-prone coverage today is concentrated in these files:
src/test/logseq/cli/command/sync_test.cljs— many async tests mutatecli-server/ensure-server!,transport/invoke, andcli-config/update-config!withset!.src/test/logseq/cli/commands_test.cljs— many async command execution tests mutatecli-server/*,transport/invoke, and add-command resolver vars.src/test/logseq/cli/server_test.cljs— async tests mutate daemon fns plus process globals likejs/process.killandchild_process.spawn.src/test/logseq/cli/command/doctor_test.cljsandsrc/test/logseq/cli/command/graph_test.cljs— smaller but repeated manual global override patterns.src/test/logseq/cli/integration_test.cljs— a mix of safep/with-redefsusage and unsafe directset!ofcli-server/ensure-server!,transport/invoke, andprocess.stderr.writeinterception.src/test/logseq/cli/transport_test.cljs— mutatesurl.parseand depends on manual restoration around async flows.src/test/frontend/worker/db_worker_node_test.cljs— async tests mutate platform/db-core/db-lock vars and also interact with module-leveldefonceatoms such as*sse-clients,*ready?,*lock-info, and*file-handlerinsrc/main/frontend/worker/db_worker_node.cljs.
The risk is not theoretical: these tests are asynchronous, share a single Node process, and run against real namespace vars or process objects. Any missing reset can change behavior of unrelated tests that run later in the same suite.
Current leak vectors
1. Manual set! / restore in async tests
The dominant pattern is:
- capture original var value,
set!the var to a mock,- run async code,
- restore in
p/finally.
This appears throughout sync_test.cljs, commands_test.cljs, server_test.cljs, doctor_test.cljs, graph_test.cljs, and parts of integration_test.cljs.
This pattern is brittle because cleanup is duplicated in every test and can drift over time.
2. Process-global overrides
Some tests override Node globals directly, for example:
child_process.spawninsrc/test/logseq/cli/server_test.cljsjs/process.killinsrc/test/logseq/cli/server_test.cljsjs/process.stderr.writeviacapture-stderr!insrc/test/logseq/cli/integration_test.cljsurl.parseinsrc/test/logseq/cli/transport_test.cljs
These are more dangerous than namespace-local mocks because they affect any code running in the same process while the override is active.
3. Module-level mutable atoms in db-worker-node
src/main/frontend/worker/db_worker_node.cljs keeps daemon state in defonce atoms:
*ready?*sse-clients*lock-info*file-handler
db_worker_node_test.cljs already has one :each fixture for print suppression, but the file still contains tests that interact with or depend on mutable daemon state. Some tests locally save and restore *sse-clients; others rely on daemon stop paths to reset internals.
This should be normalized into a single fixture-backed reset strategy so every test starts from a known baseline even if a previous test fails unexpectedly.
4. Mixed mocking styles across files
Some tests already use safer scoped mocking:
p/with-redefsinsrc/test/logseq/cli/integration_test.cljswith-redefsin targeted synchronous tests
But many nearby tests still use raw set!. The inconsistency makes future maintenance error-prone and makes it harder to know which tests are safely isolated.
Scope
This plan covers async tests for current logseq-cli and db-worker-node behavior.
In scope:
src/test/logseq/cli/*.cljssrc/test/logseq/cli/command/*.cljssrc/test/frontend/worker/db_worker_node_test.cljssrc/test/frontend/worker/db_worker_node_lock_test.cljsif follow-up cleanup is needed for consistency
Out of scope:
- Non-CLI frontend async tests unless a reusable helper extracted here is intentionally shared
- Refactoring production runtime logic unless needed only to expose test-reset hooks for daemon state
- Rewriting all sync tests that already use safe scoped mocks
Desired end state
After this work:
- every async test that mocks a namespace var uses scoped mocking or a standardized helper that guarantees restoration;
- every process-global override has one canonical helper with guaranteed teardown;
- every db-worker-node mutable singleton used by tests is reset in an
:eachfixture; - no CLI-related async test relies on state left behind by a previous test;
- the suite can run target namespaces together, repeatedly, and in different orders without order-dependent failures.
Testing Plan
I will first lock in the current risk surface by inventorying every CLI-related async test that uses set!, process-global mutation, or mutable singleton state.
I will convert one representative file in each category to a safer reset pattern before touching the rest, so the approach is proven incrementally.
I will run targeted test namespaces repeatedly and in grouped combinations to detect order-dependent leakage.
NOTE: I will write or adjust tests/helpers before broad refactors where possible, and any unexpected async failure will be debugged before continuing wide mechanical conversion.
Implementation plan
-
Create an audit checklist of all CLI-related async tests that currently use
set!, process-global mutation, manual restore logic, or mutable singleton state. -
Group those findings by leak type:
- namespace-var overrides,
- Node process/global overrides,
- db-worker-node singleton atoms,
- real server/daemon lifecycle cleanup.
-
Add a small shared test helper namespace for CLI async isolation, e.g.
src/test/logseq/cli/test_helper.cljs, to centralize patterns that should no longer be repeated inline. -
In that helper, add wrappers/macros/functions for scoped async mocking around Promesa flows so tests can replace repeated
orig-*/set!/p/finally restoreboilerplate. -
Prefer
p/with-redefsfor namespace vars that are only needed during the async body, especially in:src/test/logseq/cli/command/sync_test.cljssrc/test/logseq/cli/commands_test.cljssrc/test/logseq/cli/command/doctor_test.cljssrc/test/logseq/cli/command/graph_test.cljs- targeted sections of
src/test/logseq/cli/integration_test.cljs
-
Convert low-risk files first (
graph_test.cljs,doctor_test.cljs,main_test.cljsif needed) to establish the preferred style with minimal surface area. -
Convert
src/test/logseq/cli/command/sync_test.cljsfrom repeated manualset!restoration to scoped mocks. This file should become the reference pattern for async command tests. -
Convert
src/test/logseq/cli/commands_test.cljsin batches, prioritizing the async sections that currently mutatecli-server/list-graphs,cli-server/ensure-server!,transport/invoke, and add-command resolution vars. -
For
src/test/logseq/cli/server_test.cljs, separate two concerns:- keep real filesystem/server cleanup in
p/finally, - move mock restoration for functions/globals into scoped helper wrappers wherever possible.
- keep real filesystem/server cleanup in
-
For process-global overrides that cannot use ordinary
with-redefs(for examplechild_process.spawn,js/process.kill,process.stderr.write,url.parse), add dedicated helper functions that usetry/finallyor Promesa-aware wrappers so restoration is guaranteed from one place. -
Refactor
src/test/logseq/cli/integration_test.cljsto replace the remaining directset!mocks withp/with-redefswhere possible, since that file already demonstrates the safer pattern in other tests. -
Add or reuse an
:eachfixture insrc/test/frontend/worker/db_worker_node_test.cljsto reset db-worker-node mutable singleton atoms before and after every test:*ready?*sse-clients*lock-info*file-handler
-
Keep the existing quiet-print fixture in
db_worker_node_test.cljs, but merge or compose it with the new daemon-state reset fixture so both print behavior and daemon singletons are normalized per test. -
Review tests in
db_worker_node_test.cljsthat directly save/restore individual atoms (for example*sse-clients) and simplify them once the shared fixture guarantees a clean baseline. -
If a daemon stop path is currently relied on for cleanup, keep asserting
stop!behavior but do not rely on it as the only test isolation mechanism. -
Add comments in the helper or fixture code documenting the rule: mock restoration belongs in scoped helpers/fixtures, while resource cleanup (servers, files, daemons) belongs in
p/finally. -
Update any contributor-facing guidance near the modified tests if a recurring pattern should be preserved, especially when replacing repetitive manual restore code with helper abstractions.
-
Run targeted namespaces individually after each batch of refactors:
logseq.cli.command.graph-testlogseq.cli.command.doctor-testlogseq.cli.command.sync-testlogseq.cli.server-testlogseq.cli.transport-testlogseq.cli.commands-testlogseq.cli.integration-testfrontend.worker.db-worker-node-test
-
Run grouped combinations of the above namespaces multiple times in the same process to catch order-dependent leaks.
-
Finish with
bb dev:lint-and-testif the changed surface is stable enough and runtime permits.
Verification strategy
Use three levels of verification.
A. Static inventory verification
Re-scan CLI-related test files and confirm that leak-prone raw patterns are either gone or intentionally wrapped:
- direct
set!to namespace vars in async tests should be eliminated or heavily reduced; - any remaining direct process-global mutation must be routed through a dedicated helper with centralized restore logic;
- db-worker-node singleton atoms should be covered by fixtures rather than ad hoc local resets.
B. Repetition and order verification
Run the target namespaces repeatedly and in different orders.
At minimum:
- run each namespace alone;
- run
sync-test,commands-test, andintegration-testtogether; - run
server-test,transport-test, anddb_worker_node_testtogether; - re-run the same grouped command at least twice to catch leaked state from the previous run.
C. Behavioral smoke verification
Confirm that tests still validate real async behavior rather than only helper abstractions:
- server startup/shutdown tests still exercise real server lifecycle paths;
- integration tests still verify CLI flow outputs;
- db-worker-node tests still verify daemon and HTTP behavior;
- helper conversion does not weaken assertions or skip cleanup-sensitive paths.
Proposed helper patterns
Pattern 1: Scoped Promesa var redefs
For ordinary vars such as cli-server/ensure-server!, transport/invoke, cli-config/update-config!, and db-lock/update-lock!, prefer p/with-redefs around the async body.
This should replace bespoke orig-* capture and restoration in most test files.
Pattern 2: Process-global guard helpers
For globals like process.stderr.write, process.kill, or JS module properties such as child_process.spawn and url.parse, create helper wrappers such as:
with-stderr-write-capturewith-process-kill-mockwith-child-process-spawn-mockwith-js-property-override
These helpers should always restore the original value in a single finally path.
Pattern 3: db-worker-node state reset fixture
Create a fixture that snapshots and resets db-worker-node singleton atoms around each test. The fixture should not depend on individual tests remembering which atoms they touched.
If some atoms contain resources such as handlers or open references, the fixture should also null them out after test completion.
Edge cases to validate during implementation
If an async test fails before reaching the happy path, mock restoration must still happen.
If a mocked function is rebound inside nested async calls, the outer helper should still restore the original after the Promise chain settles.
If real daemon startup fails, fixture teardown must not throw while resetting singleton atoms.
If capture-stderr! or similar helpers are used concurrently in the future, helper APIs should make nesting/ownership behavior explicit.
If a test still needs local save/restore of atom contents for assertion purposes, that local logic should compose safely with the global fixture baseline.
Expected file touch points
Primary expected edits:
src/test/logseq/cli/command/graph_test.cljssrc/test/logseq/cli/command/doctor_test.cljssrc/test/logseq/cli/command/sync_test.cljssrc/test/logseq/cli/commands_test.cljssrc/test/logseq/cli/server_test.cljssrc/test/logseq/cli/transport_test.cljssrc/test/logseq/cli/integration_test.cljssrc/test/frontend/worker/db_worker_node_test.cljssrc/test/logseq/cli/test_helper.cljsor similar new helper namespace
Possible follow-up edits:
src/test/frontend/worker/db_worker_node_lock_test.cljsif helper reuse or fixture alignment is beneficial- any nearby docs/comments that explain the approved async mocking pattern
Non-goals
This plan does not require rewriting working synchronous tests just to match style.
This plan does not require changing production APIs unless a tiny testability hook is necessary for deterministic reset behavior.
This plan does not require introducing a new test framework; it should stay within current cljs.test and promesa patterns.
Decision
Do not add a new lint rule or automated check for this work.
Enforce the no-leaky-mocks rule by convention, shared helpers, and fixtures only.
Adopt a two-layer strategy:
- use scoped redefs/helpers for mocked functions and process globals;
- use
:eachfixtures for mutable singleton state owned bydb-worker-node.
This keeps the rollout focused on improving test isolation directly, avoids adding maintenance burden for a custom check, and still addresses both function mocking leaks and shared-state leaks.