TDD: LangGraph._topological_levels() always returns empty dict — edges from start node give all nodes non-zero in-degree #1920

Open
opened 2026-04-03 00:13:22 +00:00 by freemo · 2 comments
Owner

Metadata

  • Branch: tdd/m6-langgraph-topological-levels-empty
  • Commit Message: test(langgraph): add tdd issue-capture test for _topological_levels() always returning empty dict
  • Milestone: v3.5.0
  • Parent Epic: #390

Background and Context

The LangGraph._topological_levels() method in src/cleveragents/langgraph/graph.py (lines 80–100) always returns an empty dictionary for any graph with nodes connected from the start node.

The method correctly excludes start and end from the in_degree dict, but then counts all edges (including edges from start) when computing in-degrees. This means nodes directly connected from start have in_degree=1 instead of 0, so no nodes appear in level 0, and the BFS loop never executes — the method always returns {}.

This TDD issue captures the bug by writing a failing test (tagged @tdd_expected_fail) that proves the incorrect behavior before the fix is implemented in #1926.

Current Behavior

from cleveragents.langgraph.graph import GraphConfig, LangGraph
from cleveragents.langgraph.nodes import NodeConfig, NodeType, Edge

config = GraphConfig(
    name='layered_graph',
    nodes={
        'a': NodeConfig(name='a', type=NodeType.FUNCTION),
        'b': NodeConfig(name='b', type=NodeType.FUNCTION),
        'c': NodeConfig(name='c', type=NodeType.FUNCTION),
    },
    edges=[
        Edge(source='start', target='a'),
        Edge(source='start', target='b'),
        Edge(source='a', target='c'),
        Edge(source='b', target='c'),
        Edge(source='c', target='end'),
    ],
    entry_point='start'
)
graph = LangGraph(config=config)
levels = graph._topological_levels()
print(f'Levels: {levels}')  # Actual: {}

Actual: {} (empty dict) — BFS loop never executes because no nodes have in_degree=0.

Expected Behavior

_topological_levels() should return a dict mapping level numbers to sets of nodes at that level. For the example above: {0: {'a', 'b'}, 1: {'c'}}.

Root Cause

The method excludes start and end from the in_degree dict (correct), but still counts edges from start when computing in-degrees. The fix is to also skip edges whose source is start or end:

# Buggy:
for edge in self.config.edges:
    if edge.target in in_degree:
        in_degree[edge.target] = in_degree.get(edge.target, 0) + 1

# Fixed:
for edge in self.config.edges:
    if edge.source not in {"start", "end"} and edge.target in in_degree:
        in_degree[edge.target] = in_degree.get(edge.target, 0) + 1

TDD Test Tags

The Behave scenario must be tagged with:

  • @tdd_issue — generic filter tag (permanent)
  • @tdd_issue_1926 — links to bug issue #1926 (permanent)
  • @tdd_expected_fail — inverts result while bug is unfixed (removed by bug fix developer in #1926)

Subtasks

  • Write a Behave scenario in features/ that reproduces the bug (calls _topological_levels() on a graph with start→node edges and asserts the correct level dict is returned)
  • Tag the scenario with @tdd_issue, @tdd_issue_1926, and @tdd_expected_fail
  • Verify the test fails on the current (unfixed) code (proving the bug exists)
  • Verify the test passes CI due to the @tdd_expected_fail inversion tag
  • Run nox (all default sessions) and confirm no regressions

Definition of Done

This issue is complete when:

  • A Behave scenario capturing the _topological_levels() empty-dict bug is committed and merged to master on branch tdd/m6-langgraph-topological-levels-empty.
  • The scenario is tagged @tdd_issue, @tdd_issue_1926, and @tdd_expected_fail.
  • The test fails on the unfixed code (proving the bug) but passes CI via the @tdd_expected_fail inversion.
  • A Git commit is created where the first line of the commit message matches the Commit Message in Metadata exactly, followed by a blank line, then additional lines providing relevant details.
  • The commit is pushed to the remote on branch tdd/m6-langgraph-topological-levels-empty.
  • The commit is submitted as a pull request to master, reviewed, and merged before this issue is marked done.
  • All nox stages pass.
  • Coverage >= 97%.

Automated by CleverAgents Bot
Supervisor: UAT Testing | Agent: ca-new-issue-creator

## Metadata - **Branch**: `tdd/m6-langgraph-topological-levels-empty` - **Commit Message**: `test(langgraph): add tdd issue-capture test for _topological_levels() always returning empty dict` - **Milestone**: v3.5.0 - **Parent Epic**: #390 ## Background and Context The `LangGraph._topological_levels()` method in `src/cleveragents/langgraph/graph.py` (lines 80–100) always returns an empty dictionary for any graph with nodes connected from the `start` node. The method correctly excludes `start` and `end` from the `in_degree` dict, but then counts **all** edges (including edges from `start`) when computing in-degrees. This means nodes directly connected from `start` have `in_degree=1` instead of `0`, so no nodes appear in level 0, and the BFS loop never executes — the method always returns `{}`. This TDD issue captures the bug by writing a failing test (tagged `@tdd_expected_fail`) that proves the incorrect behavior before the fix is implemented in #1926. ## Current Behavior ```python from cleveragents.langgraph.graph import GraphConfig, LangGraph from cleveragents.langgraph.nodes import NodeConfig, NodeType, Edge config = GraphConfig( name='layered_graph', nodes={ 'a': NodeConfig(name='a', type=NodeType.FUNCTION), 'b': NodeConfig(name='b', type=NodeType.FUNCTION), 'c': NodeConfig(name='c', type=NodeType.FUNCTION), }, edges=[ Edge(source='start', target='a'), Edge(source='start', target='b'), Edge(source='a', target='c'), Edge(source='b', target='c'), Edge(source='c', target='end'), ], entry_point='start' ) graph = LangGraph(config=config) levels = graph._topological_levels() print(f'Levels: {levels}') # Actual: {} ``` **Actual**: `{}` (empty dict) — BFS loop never executes because no nodes have `in_degree=0`. ## Expected Behavior `_topological_levels()` should return a dict mapping level numbers to sets of nodes at that level. For the example above: `{0: {'a', 'b'}, 1: {'c'}}`. ## Root Cause The method excludes `start` and `end` from the `in_degree` dict (correct), but still counts edges **from** `start` when computing in-degrees. The fix is to also skip edges whose source is `start` or `end`: ```python # Buggy: for edge in self.config.edges: if edge.target in in_degree: in_degree[edge.target] = in_degree.get(edge.target, 0) + 1 # Fixed: for edge in self.config.edges: if edge.source not in {"start", "end"} and edge.target in in_degree: in_degree[edge.target] = in_degree.get(edge.target, 0) + 1 ``` ## TDD Test Tags The Behave scenario must be tagged with: - `@tdd_issue` — generic filter tag (permanent) - `@tdd_issue_1926` — links to bug issue #1926 (permanent) - `@tdd_expected_fail` — inverts result while bug is unfixed (removed by bug fix developer in #1926) ## Subtasks - [ ] Write a Behave scenario in `features/` that reproduces the bug (calls `_topological_levels()` on a graph with `start`→node edges and asserts the correct level dict is returned) - [ ] Tag the scenario with `@tdd_issue`, `@tdd_issue_1926`, and `@tdd_expected_fail` - [ ] Verify the test **fails** on the current (unfixed) code (proving the bug exists) - [ ] Verify the test **passes** CI due to the `@tdd_expected_fail` inversion tag - [ ] Run `nox` (all default sessions) and confirm no regressions ## Definition of Done This issue is complete when: - A Behave scenario capturing the `_topological_levels()` empty-dict bug is committed and merged to `master` on branch `tdd/m6-langgraph-topological-levels-empty`. - The scenario is tagged `@tdd_issue`, `@tdd_issue_1926`, and `@tdd_expected_fail`. - The test fails on the unfixed code (proving the bug) but passes CI via the `@tdd_expected_fail` inversion. - A Git commit is created where the **first line** of the commit message matches the Commit Message in Metadata exactly, followed by a blank line, then additional lines providing relevant details. - The commit is pushed to the remote on branch `tdd/m6-langgraph-topological-levels-empty`. - The commit is submitted as a **pull request** to `master`, reviewed, and **merged** before this issue is marked done. - All nox stages pass. - Coverage >= 97%. --- **Automated by CleverAgents Bot** Supervisor: UAT Testing | Agent: ca-new-issue-creator
freemo added this to the v3.5.0 milestone 2026-04-03 00:15:03 +00:00
Author
Owner

⚠️ Potential duplicate notice: This issue (#1920 TDD) describes the same bug as #1926 (UAT: LangGraph._topological_levels() always returns empty dict). Both report that edges from the start node cause all nodes to have non-zero in-degree, resulting in an empty topological levels dict.

Please review both issues and consider closing one as a duplicate. The UAT issue (#1926) may be the canonical one as it was filed from user acceptance testing.

Label compliance fix also applied:

  • Replaced repo-level labels with org-level canonical labels: Priority/Critical, State/Unverified, Type/Testing

Automated by CleverAgents Bot
Supervisor: Backlog Grooming | Agent: ca-backlog-groomer

⚠️ **Potential duplicate notice**: This issue (#1920 TDD) describes the same bug as #1926 (UAT: `LangGraph._topological_levels()` always returns empty dict). Both report that edges from the start node cause all nodes to have non-zero in-degree, resulting in an empty topological levels dict. Please review both issues and consider closing one as a duplicate. The UAT issue (#1926) may be the canonical one as it was filed from user acceptance testing. Label compliance fix also applied: - Replaced repo-level labels with org-level canonical labels: `Priority/Critical`, `State/Unverified`, `Type/Testing` --- **Automated by CleverAgents Bot** Supervisor: Backlog Grooming | Agent: ca-backlog-groomer
Author
Owner

Issue triaged by project owner:

  • State: Verified
  • Priority: Critical — TDD failing test for the topological levels bug (#1926). Per project methodology, the failing test must be written first.
  • Milestone: v3.5.0 (already assigned — milestone is past due)
  • MoSCoW: Must Have — This is the TDD companion to #1926. Both must be completed for v3.5.0.

Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: ca-project-owner

Issue triaged by project owner: - **State**: Verified ✅ - **Priority**: Critical — TDD failing test for the topological levels bug (#1926). Per project methodology, the failing test must be written first. - **Milestone**: v3.5.0 (already assigned — milestone is past due) - **MoSCoW**: Must Have — This is the TDD companion to #1926. Both must be completed for v3.5.0. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: ca-project-owner
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
cleveragents/cleveragents-core#1920
No description provided.