Files
logseq/docs/agent-guide/053-logseq-cli-async-test-isolation.md
2026-03-12 15:12:34 +08:00

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 mutate cli-server/ensure-server!, transport/invoke, and cli-config/update-config! with set!.
  • src/test/logseq/cli/commands_test.cljs — many async command execution tests mutate cli-server/*, transport/invoke, and add-command resolver vars.
  • src/test/logseq/cli/server_test.cljs — async tests mutate daemon fns plus process globals like js/process.kill and child_process.spawn.
  • src/test/logseq/cli/command/doctor_test.cljs and src/test/logseq/cli/command/graph_test.cljs — smaller but repeated manual global override patterns.
  • src/test/logseq/cli/integration_test.cljs — a mix of safe p/with-redefs usage and unsafe direct set! of cli-server/ensure-server!, transport/invoke, and process.stderr.write interception.
  • src/test/logseq/cli/transport_test.cljs — mutates url.parse and 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-level defonce atoms such as *sse-clients, *ready?, *lock-info, and *file-handler in src/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:

  1. capture original var value,
  2. set! the var to a mock,
  3. run async code,
  4. 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.spawn in src/test/logseq/cli/server_test.cljs
  • js/process.kill in src/test/logseq/cli/server_test.cljs
  • js/process.stderr.write via capture-stderr! in src/test/logseq/cli/integration_test.cljs
  • url.parse in src/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-redefs in src/test/logseq/cli/integration_test.cljs
  • with-redefs in 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/*.cljs
  • src/test/logseq/cli/command/*.cljs
  • src/test/frontend/worker/db_worker_node_test.cljs
  • src/test/frontend/worker/db_worker_node_lock_test.cljs if 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 :each fixture;
  • 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

  1. Create an audit checklist of all CLI-related async tests that currently use set!, process-global mutation, manual restore logic, or mutable singleton state.

  2. Group those findings by leak type:

    • namespace-var overrides,
    • Node process/global overrides,
    • db-worker-node singleton atoms,
    • real server/daemon lifecycle cleanup.
  3. 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.

  4. In that helper, add wrappers/macros/functions for scoped async mocking around Promesa flows so tests can replace repeated orig-* / set! / p/finally restore boilerplate.

  5. Prefer p/with-redefs for namespace vars that are only needed during the async body, especially in:

    • src/test/logseq/cli/command/sync_test.cljs
    • src/test/logseq/cli/commands_test.cljs
    • src/test/logseq/cli/command/doctor_test.cljs
    • src/test/logseq/cli/command/graph_test.cljs
    • targeted sections of src/test/logseq/cli/integration_test.cljs
  6. Convert low-risk files first (graph_test.cljs, doctor_test.cljs, main_test.cljs if needed) to establish the preferred style with minimal surface area.

  7. Convert src/test/logseq/cli/command/sync_test.cljs from repeated manual set! restoration to scoped mocks. This file should become the reference pattern for async command tests.

  8. Convert src/test/logseq/cli/commands_test.cljs in batches, prioritizing the async sections that currently mutate cli-server/list-graphs, cli-server/ensure-server!, transport/invoke, and add-command resolution vars.

  9. 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.
  10. For process-global overrides that cannot use ordinary with-redefs (for example child_process.spawn, js/process.kill, process.stderr.write, url.parse), add dedicated helper functions that use try/finally or Promesa-aware wrappers so restoration is guaranteed from one place.

  11. Refactor src/test/logseq/cli/integration_test.cljs to replace the remaining direct set! mocks with p/with-redefs where possible, since that file already demonstrates the safer pattern in other tests.

  12. Add or reuse an :each fixture in src/test/frontend/worker/db_worker_node_test.cljs to reset db-worker-node mutable singleton atoms before and after every test:

    • *ready?
    • *sse-clients
    • *lock-info
    • *file-handler
  13. 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.

  14. Review tests in db_worker_node_test.cljs that directly save/restore individual atoms (for example *sse-clients) and simplify them once the shared fixture guarantees a clean baseline.

  15. 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.

  16. 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.

  17. 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.

  18. Run targeted namespaces individually after each batch of refactors:

    • logseq.cli.command.graph-test
    • logseq.cli.command.doctor-test
    • logseq.cli.command.sync-test
    • logseq.cli.server-test
    • logseq.cli.transport-test
    • logseq.cli.commands-test
    • logseq.cli.integration-test
    • frontend.worker.db-worker-node-test
  19. Run grouped combinations of the above namespaces multiple times in the same process to catch order-dependent leaks.

  20. Finish with bb dev:lint-and-test if 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, and integration-test together;
  • run server-test, transport-test, and db_worker_node_test together;
  • 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-capture
  • with-process-kill-mock
  • with-child-process-spawn-mock
  • with-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.cljs
  • src/test/logseq/cli/command/doctor_test.cljs
  • src/test/logseq/cli/command/sync_test.cljs
  • src/test/logseq/cli/commands_test.cljs
  • src/test/logseq/cli/server_test.cljs
  • src/test/logseq/cli/transport_test.cljs
  • src/test/logseq/cli/integration_test.cljs
  • src/test/frontend/worker/db_worker_node_test.cljs
  • src/test/logseq/cli/test_helper.cljs or similar new helper namespace

Possible follow-up edits:

  • src/test/frontend/worker/db_worker_node_lock_test.cljs if 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:

  1. use scoped redefs/helpers for mocked functions and process globals;
  2. use :each fixtures for mutable singleton state owned by db-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.