Skip to content

test: assert the client returns a valid execution plan, not which plan the engine picked - #609

Open
Naseem77 wants to merge 10 commits into
mainfrom
naseem77-align-plan-assertions-with-engine
Open

test: assert the client returns a valid execution plan, not which plan the engine picked#609
Naseem77 wants to merge 10 commits into
mainfrom
naseem77-align-plan-assertions-with-engine

Conversation

@Naseem77

@Naseem77 Naseem77 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

The plan specs pinned exact operation names, arguments and indentation, so any change to how the engine plans a query broke them even though the client was behaving correctly. That is testing the engine's planner, not the client.

Assert what is actually the client's responsibility instead: a plan comes back, it holds at least as many operations as the query needs, every line names an operation, and the indentation that conveys nesting is well formed. Profiles additionally have to carry per operation statistics, and the row count the query yields is still checked by value since that is a property of the query rather than of the engine.

The new assertions live in tests/planHelpers.ts. They are not a rubber stamp — each one was verified to reject a malformed plan: an empty reply, too few operations, a blank operation name, a half indent, an indent that skips a level, a root that is indented, a profile missing its statistics, and a wrong row count.

Also drops a RETURN exists((a)-[:KNOWS]->(b)) from the constraint test. Its result was never asserted, so the call only added a dependency on a function that behaves differently between engines without testing anything.

Separately, the slowlog tests queried 200k rows, which runs in roughly 6ms and so falls under the ~10ms slowlog threshold. Bumped to 1M rows so the query is actually recorded.

Verification

graphAndQuery, profile and constraints: 35 passed against both engines, and the slowlog tests pass on both. Lint is clean.

Not covered here

Two groups of failures on edge are engine side decisions and are deliberately left alone: the GRAPH.INFO specs, and setting CMD_INFO / MAX_INFO_QUERIES at run time.

Summary by CodeRabbit

  • Tests
    • Improved validation of query execution plans and profiling results across supported engines.
    • Added checks for operation structure, nesting, record counts, and engine-specific variations.
    • Expanded slow-query workloads to improve reliability of performance and logging checks.
    • Simplified relationship-constraint test coverage while preserving essential validation.
    • Added shared validation utilities for consistent plan and profile assertions.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3a4feb25-b3e8-43fd-a17e-371cbf2f8f71

📥 Commits

Reviewing files that changed from the base of the PR and between 0169bd2 and ecaf311.

📒 Files selected for processing (4)
  • .github/workflows/node.js.yml
  • tests/graphAndQuery.spec.ts
  • tests/planHelpers.ts
  • tests/profile.spec.ts

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Added shared helpers for execution-plan and profile validation. Updated graph, query, and profile tests to use the helpers. Increased slow-query workloads from 200,000 to 1,000,000. Removed legacy Docker Compose installation from CI and simplified a relationship-creation query.

Changes

Query test updates

Layer / File(s) Summary
Shared plan validation and test migration
tests/planHelpers.ts, tests/graphAndQuery.spec.ts, tests/profile.spec.ts
Added helpers for plan shape, nesting, operation names, profile statistics, and record counts. Updated execution-plan and profile tests to use them.
Slow-query workload updates
tests/graphAndQuery.spec.ts, tests/cluster.spec.ts, tests/single.spec.ts
Increased UNWIND workloads from 200,000 to 1,000,000 while retaining existing result and log assertions.
CI setup cleanup
.github/workflows/node.js.yml
Removed installation of the legacy docker-compose client.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: 🔵 Low · up to ecaf3

The PR improves plan and slowlog assertions, but the slowlog tests still depend on shared log state and the first result, which could cause flaky or misleading test outcomes; the change is mergeable with explicit owner awareness or follow-up.

Suggested reviewers: anchel123, gkorland

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: tests now validate execution-plan validity without requiring an engine-specific plan.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch naseem77-align-plan-assertions-with-engine

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/graphAndQuery.spec.ts`:
- Around line 336-337: Update the single-query plan assertions in the relevant
test to verify the GRAPH.EXPLAIN array’s exact leading sequence: assert
“Project” is the first entry and “    Node By Label Scan | (n:Person)” is the
second, rather than using toContain; ensure the assertion also fails if an
unexpected “Results” step is present.
- Line 277: Update the slowlog test around longQuery to clear the graph slowlog
before execution, then assert that the slowlog results contain an entry for this
query rather than assuming slowLogResults[0] is the match. Keep the assertion
host-speed independent while preserving validation that the long query is
logged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dfd8c575-51fd-47a2-944b-acc2d36257ce

📥 Commits

Reviewing files that changed from the base of the PR and between c009aec and 62a3798.

📒 Files selected for processing (2)
  • tests/graphAndQuery.spec.ts
  • tests/profile.spec.ts

Comment thread tests/graphAndQuery.spec.ts
Comment thread tests/graphAndQuery.spec.ts Outdated
…n the engine picked

The specs pinned exact operation names, arguments and indentation, so any
change to how the engine plans a query broke them even though the client
was behaving correctly. Which operations a query compiles into is the
engine's business.

Assert what is actually the client's responsibility instead: a plan comes
back, it holds at least as many operations as the query needs, every line
names an operation, and the indentation that conveys nesting is well
formed. Profiles additionally have to carry per operation statistics, and
the row count the query yields is checked since that is a property of the
query rather than of the engine.

These specs now pass against both engines.
…t test

The RETURN exists((a)-[:KNOWS]->(b)) result was never asserted, so the
call only added a dependency on a function that behaves differently
between engines without testing anything.
@Naseem77
Naseem77 force-pushed the naseem77-align-plan-assertions-with-engine branch from 7503940 to 0169bd2 Compare August 10, 2026 14:01
@Naseem77 Naseem77 changed the title test: align execution plan assertions with the current engine test: assert the client returns a valid execution plan, not which plan the engine picked Aug 10, 2026
Copilot AI lite review requested due to automatic review settings August 16, 2026 10:45
@codecov

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.45%. Comparing base (113c4b5) to head (ecaf311).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #609      +/-   ##
==========================================
+ Coverage   97.32%   97.45%   +0.12%     
==========================================
  Files          28       29       +1     
  Lines        2204     2316     +112     
  Branches      319      140     -179     
==========================================
+ Hits         2145     2257     +112     
- Misses         58       59       +1     
+ Partials        1        0       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the test suite to validate execution plans/profiles in an engine-agnostic way, focusing on what the client must return rather than pinning exact planner output.

Changes:

  • Introduces tests/planHelpers.ts with reusable assertions for GRAPH.EXPLAIN and GRAPH.PROFILE output shape and nesting.
  • Refactors existing plan/profile specs to use the new helper assertions instead of exact operation-name matching.
  • Adjusts slowlog tests to use a larger workload and removes an unasserted RETURN exists(...) from the constraints test.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/single.spec.ts Increases slowlog query workload to better trigger slowlog recording.
tests/profile.spec.ts Replaces exact plan-step assertions with expectProfile(...) helper checks.
tests/planHelpers.ts Adds engine-agnostic execution plan/profile validation helpers.
tests/graphAndQuery.spec.ts Switches explain-plan assertions to expectExecutionPlan(...) and increases slowlog workload.
tests/constraints.spec.ts Removes an unasserted RETURN exists(...) expression from a constraint setup query.
tests/cluster.spec.ts Increases slowlog query workload to better trigger slowlog recording.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tests/planHelpers.ts
Comment thread tests/planHelpers.ts
Copilot AI review requested due to automatic review settings August 18, 2026 20:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (4)

tests/planHelpers.ts:37

  • The indentation assertion uses toBe(index === 0 ? 0 : Math.min(...)), which is harder to read and can still throw if the previous entry is not a string. Splitting the root vs non-root cases and using toBeLessThanOrEqual makes the intent clearer and keeps the helper from crashing on malformed input.
    // indentation is what conveys nesting, so it has to be whole levels, and
    // an operation can only ever be one level deeper than the one above it
    const indent = indentOf(line);
    expect(Number.isInteger(indent)).toBe(true);
    expect(indent).toBe(index === 0 ? 0 : Math.min(indent, indentOf(plan[index - 1]) + 1));

tests/planHelpers.ts:52

  • The profile time regex requires a decimal fraction (\d+\.\d+). If the engine reports an integer value (e.g. Execution time: 1 ms) this helper will fail even though the profile is valid. Making the fractional part optional keeps the assertion engine-agnostic while still rejecting missing timing stats.
    const records = line.match(/Records produced: (\d+)/);
    const time = line.match(/Execution time: (\d+\.\d+) ms/);

tests/planHelpers.ts:28

  • expectExecutionPlan asserts typeof line === "string" but then immediately calls operationName(line)/indentOf(line). If a malformed reply contains a non-string entry, this will throw a TypeError instead of producing a clear assertion failure (and it can also break the rest of the validation). Add a runtime guard after the type assertion to keep the helper reporting a test failure rather than crashing.
    expect(typeof line).toBe("string");

    // every line names an operation, optionally followed by its arguments
    expect(operationName(line)).not.toBe("");

tests/planHelpers.ts:63

  • expectProfile claims to verify the number of rows yielded by the query, but it currently checks Math.max(...counts). In profiled plans, intermediate operations (e.g., scans) can produce more records than the final result after filters/limits, so max is not equivalent to the query result count. Since expectExecutionPlan already enforces the first line is the root (indent 0), assert against counts[0] instead.
  if (recordsProduced !== undefined) {
    // how many rows the query yields is a property of the query, not of the
    // engine, so the client must report it whichever engine answered
    expect(Math.max(...counts)).toBe(recordsProduced);
  }

Co-Authored-By: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 19, 2026 08:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (2)

tests/planHelpers.ts:43

  • expectExecutionPlan can throw (e.g., plan.length, plan.forEach, operationName(line)) even when it’s meant to assert malformed replies. Because Jest expect() doesn’t stop execution, a non-array plan, an empty plan, or a non-string line will cause a TypeError and hide the real failure. Add early returns / guards so the helper fails with assertions instead of crashing.
export function expectExecutionPlan(plan: string[], minOperations = 1): void {
  expect(Array.isArray(plan)).toBe(true);
  expect(plan.length).toBeGreaterThanOrEqual(minOperations);

  plan.forEach((line, index) => {
    expect(typeof line).toBe("string");

    // every line names an operation, optionally followed by its arguments
    expect(operationName(line)).not.toBe("");

    // indentation is what conveys nesting, so it has to be whole levels, and
    // an operation can only ever be one level deeper than the one above it
    const indent = indentOf(line);
    expect(Number.isInteger(indent)).toBe(true);
    expect(indent).toBe(index === 0 ? 0 : Math.min(indent, indentOf(plan[index - 1]) + 1));
  });

tests/planHelpers.ts:12

  • PROFILE_STATS is currently very strict (requires a comma + \d+\.\d+ ms). That makes operationLine() stripping fragile: if the engine outputs a valid profile with a slightly different time format, expectPlanShape will start comparing operation+args including stats and fail. Relax the stripping regex to remove everything from | Records produced: to EOL; expectProfile() already validates the stats separately.
const INDENT = "    ";
const ENGINE_ROOT_OPERATIONS = new Set(["Results", "Commit"]);
const PROFILE_STATS = /\s*\|\s*Records produced: \d+,\s*Execution time: \d+\.\d+ ms\s*$/;

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 20, 2026 10:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (6)

tests/planHelpers.ts:41

  • The indentation assertion uses Math.min(indent, prevIndent + 1), which obscures the intent and yields less clear failures. A direct toBeLessThanOrEqual(prevIndent + 1) expresses the constraint (indent may only increase by 1 level) more clearly.
    expect(indent).toBe(index === 0 ? 0 : Math.min(indent, indentOf(plan[index - 1]) + 1));

tests/planHelpers.ts:88

  • The profile time assertion only matches values with a fractional part (e.g. 0.01 ms). If an engine returns 0 ms / 1 ms, this will incorrectly fail even though the profile contains valid timing data.
    const time = line.match(/Execution time: (\d+\.\d+) ms/);

tests/single.spec.ts:254

  • The test indexes slowLog[0] without asserting the slow log contains any entries. If the query isn't recorded (e.g. threshold not exceeded in a fast CI run), this will fail with a TypeError instead of a clear assertion failure.
        const slowLog = await graph.slowLog();
        expect(Array.isArray(slowLog)).toBe(true);
        expect(slowLog[0].command).toBe("GRAPH.QUERY");
        expect(slowLog[0].query).toBe(longQuery);

tests/cluster.spec.ts:199

  • The test indexes result[0] without asserting the slow log contains any entries. If nothing is recorded (e.g. the query still runs under the threshold), this will fail with an unhelpful TypeError.
                const result = await graph.slowLog();
                expect(Array.isArray(result)).toBe(true);
                expect(result[0].command).toBe("GRAPH.QUERY");
                expect(result[0].query).toBe(longQuery);

tests/planHelpers.ts:12

  • PROFILE_STATS and the profile-time matcher only accept timings with a fractional part (e.g. 1.23 ms) and will fail on valid outputs like 1 ms. Making the fractional part optional and allowing additional trailing stats makes these assertions more resilient to engine formatting changes while still enforcing presence of the stats.
const PROFILE_STATS = /\s*\|\s*Records produced: \d+,\s*Execution time: \d+\.\d+ ms\s*$/;

tests/graphAndQuery.spec.ts:281

  • The test assumes slowLogResults[0] exists. If the slow log is empty (e.g. threshold not exceeded on a fast run), this can throw before producing a clear Jest assertion failure. Assert the array is non-empty before indexing.
    const slowLogResults = await graph.slowLog();

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants