Skip to content

test_python_neo4j_backend.py writes to whatever CLDK_TEST_NEO4J_URI points at, and never cleans up #324

Closed

Description

@rahlk

Problem

tests/analysis/python/test_python_neo4j_backend.py is presented as a read-only parity test, but its
backends fixture writes a whole application into the target database:

opts = AnalysisOptions(
    input=proj,
    emit=EmitTarget.NEO4J,
    app_name=APP_NAME,
    rebuild_analysis=True,
    neo4j_uri=NEO4J_URI,      # <- $CLDK_TEST_NEO4J_URI
    ...
)
with Codeanalyzer(opts) as az:
    emit_neo4j(az.analyze(), opts)

(tests/analysis/python/test_python_neo4j_backend.py:145-165)

Two things make this a trap rather than a documented cost:

  1. The write is invisible to inspection. There is no literal CREATE/MERGE/SET/DELETE
    anywhere in the file — the write goes through a library call. Anyone auditing the test suite for
    destructive operations by searching for Cypher keywords will conclude the file is read-only. It is not.
  2. There is no teardown. The fixture yields and then only calls neo.close(). The emitted
    application (APP_NAME) stays in the database after the run.

Impact

Anyone who has CLDK_TEST_NEO4J_URI exported — which is exactly the setup used to run the live
end-to-end tests against a real analysis — and then runs the python test directory will silently
inject a second application into that database.

This already happened during leg 1.5 development: a run against the integration database (holding a
~50-minute analysis of a real project) added a second application, which broke three live tests whose
expectation queries count nodes without scoping to an application. Repair required identifying and
deleting the injected nodes by hand.

Suggested fixes (any one helps; the first two are the important ones)

  • Isolate the target. Have this file require a separate env var (e.g. CLDK_TEST_NEO4J_WRITE_URI)
    so pointing at a read-only integration database is not sufficient to trigger a write. Skip the module
    when that var is unset.
  • Tear down. Delete the emitted application in fixture teardown, scoped by app_name, so a run
    leaves the database as it found it.
  • Label the hazard. Say plainly in the module docstring and in any test README that this file
    MUTATES the target database. The current docstring says the harness "loads the graph out of band"
    and then "queries it read-only", which reads as if the whole file is read-only.
  • Consider a pytest marker (e.g. @pytest.mark.destructive) deselected by default, so the write
    requires an explicit opt-in.

Related

The mirror-image defect exists on the read side: tests/analysis/python/test_e2e_neo4j_live.py has
three expectation queries that count nodes without scoping to an application, which is why a second
application turned them red instead of leaving them alone. Worth fixing together.

Definition of done

  • Running the full python test directory with only the read-only integration env vars set performs
    no writes to that database.
  • If the write path is invoked deliberately, the database is left as it was found.
  • The hazard is stated in the module docstring, not merely implied.

Caveats

  • Changing the env var this file keys off will break any existing CI job that runs it; check for one
    before renaming.

Activity

  1. rahlk commented on Sep 5, 2026

    @rahlk
    CollaboratorAuthor

    Worse than first reported: unsetting the env vars does not disable the write, it redirects it

    The original report assumed CLDK_TEST_NEO4J_URI had to be set for the write to reach a real database.
    That is wrong. Lines 46-48:

    NEO4J_URI = os.environ.get("CLDK_TEST_NEO4J_URI", "bolt://localhost:7687")
    NEO4J_USER = os.environ.get("CLDK_TEST_NEO4J_USER", "neo4j")
    NEO4J_PASSWORD = os.environ.get("CLDK_TEST_NEO4J_PASSWORD", "neo4j")
    APP_NAME = "cldk_py_parity"

    With the env unset the module does not skip by construction. It attempts verify_connectivity()
    against localhost:7687 with the default neo4j/neo4j credentials, and on success its fixture runs
    emit_neo4j(...) and writes the cldk_py_parity application there.

    Port 7687 is the Neo4j default, so anything a developer has bound or forwarded to it becomes the target
    — including an SSH port-forward to a remote or shared Neo4j. On one developer machine during leg 1.5,
    7687 was held by an ssh process in LISTEN while the local test container was deliberately on 7688.
    Under those conditions "unset the variables to be safe" sends the write off-box rather than preventing it.

    This raises the severity: the hazard is not merely "a developer who opted in loses their integration
    database", it is "a test run with no configuration at all can write to whatever answers on the default
    port with default credentials".

    Additional requirement for the fix

    Beyond the fixes already listed, the module must skip when it is not explicitly configured. It should
    not fall back to a default URI or default credentials at all — no env var, no run. An opt-in write test
    that defaults to writing somewhere is the whole problem:

    NEO4J_URI = os.environ.get("CLDK_TEST_NEO4J_WRITE_URI")
    pytestmark = pytest.mark.skipif(not NEO4J_URI, reason="set CLDK_TEST_NEO4J_WRITE_URI to run; this test WRITES")

    Silent connection to a default endpoint is also worth auditing for elsewhere in the suite.

  2. added 2 commits that reference this issue on Sep 6, 2026
    a47fabb
    ca5ed1f
  3. rahlk commented on Sep 6, 2026

    @rahlk
    CollaboratorAuthor

    Resolved in #323 (merged to release/2.0 as b5a18b4, shipped in 2.0.0-rc.2): test_python_neo4j_backend.py now skips unless CLDK_TEST_NEO4J_WRITE_URI / _WRITE_USER / _WRITE_PASSWORD are set (no default URI or credentials — a47fabb), its docstring leads with the fact that it writes, and a two-statement teardown removes what the emitter created, anchored on the application name and the can:// id prefix (ca5ed1f; the 664 shared package/decorator nodes are deliberately left, documented). The mirror-image defect — unscoped expectation counts in test_e2e_neo4j_live.py — was fixed in the same leg. Running the python test directory with only the read-only integration variables set performs no writes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions