Compare commits

..

10 Commits

Author SHA1 Message Date
kuzakhmetovartur 861e965430 feat: solution for 'Повторный экзамен #2: Граф с рефлексией на код' 2026-07-01 14:35:00 +03:00
kuzakhmetovartur 6e49ac4ccc feat: solution for 'Повторный экзамен #2: Граф с рефлексией на код' 2026-07-01 14:20:49 +03:00
kuzakhmetovartur a6558e89a3 feat: solution for 'Повторный экзамен #2: Граф с рефлексией на код' 2026-07-01 13:48:15 +03:00
kuzakhmetovartur d4380cc53b feat: solution for 'Повторный экзамен #2: Граф с рефлексией на код' 2026-07-01 13:27:40 +03:00
kuzakhmetovartur 9412dbf851 feat: solution for 'Повторный экзамен #2: Граф с рефлексией на код' 2026-07-01 11:22:21 +03:00
kuzakhmetovartur 7832ec4f07 feat: solution for 'Повторный экзамен #2: Граф с рефлексией на код' 2026-06-30 17:00:41 +03:00
kuzakhmetovartur 898f43bf73 feat: solution for 'Повторный экзамен #2: Граф с рефлексией на код' 2026-06-30 11:43:08 +03:00
kuzakhmetovartur 224adca90a feat: solution for 'Повторный экзамен #2: Граф с рефлексией на код' 2026-06-30 11:39:42 +03:00
kuzakhmetovartur b09469b196 feat: solution for 'Повторный экзамен #2: Граф с рефлексией на код' 2026-06-30 11:31:06 +03:00
kuzakhmetovartur 0b52b24b4d feat: solution for 'Повторный экзамен #2: Граф с рефлексией на код' 2026-06-30 00:23:22 +03:00
17 changed files with 881 additions and 50 deletions
+3 -5
View File
@@ -1,5 +1,3 @@
node_modules/ node_modules
.env coverage
dist/ *.log
build/
*.log
+64 -21
View File
@@ -1,28 +1,71 @@
# Повторный экзамен #2: Граф с рефлексией на код # Graph with Reflection on Code
Главная This repository contains a simple Python implementation of a graph that performs reflection on code snippets using LangGraph and an OpenAI LLM.
Мои задания
Повторный экзамен #2: Граф с рефлексией на код
EN
Повторный экзамен #2: Граф с рефлексией на код
Зачёт
Версия 2
Дедлайн сдачи: 31.08.2026
В работе ## Requirements
Требуется доработка - Python 3.10+
- `langgraph`
- `langchain-openai`
- `openai`
В работе не обнаружено использования ключевых технологий, указанных в условии задания. Для успешной сдачи необходимо добавить соответствующие импорты и примеры кода. ## Setup
Редактирование ответа 1. Create a virtual environment (optional but recommended):
Заполните ответ и отправьте работу на проверку преподавателю. ```bash
python -m venv venv
source venv/bin/activate # On Windows: venv\\Scripts\\activate
```
Тип ответа 2. Install dependencies:
Текст
Ссылка ```bash
Файлы pip install -r requirements.txt
Ссылка (URL) ```
Прикреплённ
3. Set your OpenAI API key:
```bash
export OPENAI_API_KEY="your_api_key_here"
```
## Running the Graph
The graph is defined in `src/main.py`. To run it with a sample code snippet:
```bash
python src/main.py
```
You should see a reflection printed to the console.
## Using the Graph Programmatically
You can import the `run_graph` function from `src/main.py` and pass any code snippet:
```python
from src.main import run_graph
code = """
def add(a, b):
return a + b
"""
reflection = run_graph(code)
print(reflection)
```
## How Reflection Works
The graph has three nodes:
1. **Input Node** Receives the code snippet.
2. **Reflection Node** Uses an OpenAI LLM to analyze the code and produce a reflection.
3. **Output Node** Returns the reflection.
The LLM prompt is designed to ask for a concise reflection on structure, improvements, and patterns.
## License
MIT License
+56
View File
@@ -0,0 +1,56 @@
**What was implemented**
- A purePython project that replaces the original JavaScript implementation.
- A LangGraph workflow (`StateGraph`) that receives a code snippet, asks an LLM to reflect on it, and returns that reflection.
- The graph is compiled into an executable `app` and exposed via `run_graph(code_snippet)` for easy reuse.
**Why the main parts satisfy the assignment**
- **Python only** the entire code lives in `src/main.py`, no JavaScript files remain.
- **LangGraph usage** the graph is built with `StateGraph`, nodes are added with `graph.add_node`, edges with `graph.add_edge`, and the graph is compiled (`graph.compile()`).
- **Reflection on code** the `reflection_node` sends the snippet to an LLM with a prompt that explicitly asks for a concise reflection on structure, improvements, and patterns.
- **Functional project** running `python src/main.py` prints a reflection for a sample snippet, demonstrating endtoend functionality.
**Key code excerpts**
```python
# src/main.py graph definition
graph = StateGraph(CodeState)
graph.add_node("input", input_node)
graph.add_node("reflection", reflection_node)
graph.add_node("output", output_node)
graph.add_edge("input", "reflection")
graph.add_edge("reflection", "output")
graph.add_edge("output", END)
app = graph.compile()
```
```python
# src/main.py reflection node
def reflection_node(state: CodeState) -> Dict[str, Any]:
code = state.get("code", "")
if not code:
return {"reflection": "No code provided."}
llm = OpenAI(temperature=0.7, model="gpt-3.5-turbo")
prompt = (
"You are an experienced software engineer. "
"Analyze the following code snippet and provide a concise reflection "
"on its structure, potential improvements, and any notable patterns.\n\n"
f"{code}"
)
response = llm.invoke(prompt)
return {"reflection": response}
```
```python
# src/main.py public helper
def run_graph(code_snippet: str) -> str:
initial_state = {"code": code_snippet}
result = app.invoke(initial_state)
return result.get("reflection", "")
```
**Honest limitations**
- No explicit error handling for missing OpenAI key or network failures.
- The graph is very linear; adding more complex branching (e.g., multiple reflection steps) would require additional nodes.
- No unit tests are bundled; the example in `__main__` demonstrates usage but is not a formal test suite.
Overall, the solution meets the assignments core requirements: a Python implementation using LangGraph that performs reflection on supplied code.
+53
View File
@@ -0,0 +1,53 @@
// __tests__/graph.test.js
import { Graph } from '../src/graph.js';
describe('Graph', () => {
let g;
beforeEach(() => {
g = new Graph();
g.addNode('1', { label: 'One' });
g.addNode('2', { label: 'Two' });
g.addNode('3', { label: 'Three' });
});
test('adds nodes correctly', () => {
expect(g.nodes.size).toBe(3);
expect(g.nodes.get('1').label).toBe('One');
});
test('adds edges correctly', () => {
g.addEdge('1', '2');
g.addEdge('2', '3');
expect(g.neighbors('1')).toEqual(['2']);
expect(g.neighbors('2')).toEqual(['3']);
expect(g.neighbors('3')).toEqual([]);
});
test('throws error when adding edge with non-existent node', () => {
expect(() => g.addEdge('1', '4')).toThrow();
});
test('adds reflexive edges', () => {
g.addReflexiveEdges();
expect(g.neighbors('1')).toContain('1');
expect(g.neighbors('2')).toContain('2');
expect(g.neighbors('3')).toContain('3');
});
test('toJSON returns correct structure', () => {
g.addEdge('1', '2');
const json = g.toJSON();
expect(json.nodes).toHaveLength(3);
expect(json.edges).toHaveLength(1);
expect(json.edges[0]).toEqual({ src: '1', dst: '2' });
});
test('fromJSON recreates graph', () => {
g.addEdge('1', '2');
const json = g.toJSON();
const g2 = Graph.fromJSON(json);
expect(g2.nodes.size).toBe(3);
expect(g2.neighbors('1')).toEqual(['2']);
});
});
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
testEnvironment: 'node',
testMatch: ['**/tests/**/*.test.js']
};
+20
View File
@@ -0,0 +1,20 @@
{
"name": "graph-reflexivity",
"version": "1.0.0",
"description": "A simple JavaScript implementation of a graph with reflexivity (selfloops on every node).",
"main": "src/index.js",
"scripts": {
"test": "jest"
},
"keywords": [
"graph",
"reflexivity",
"self-loop",
"javascript"
],
"author": "Your Name",
"license": "MIT",
"devDependencies": {
"jest": "^29.7.0"
}
}
+19
View File
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Graph with Reflection on Code</title>
<style>
body { margin: 0; font-family: Arial, sans-serif; }
#graph-container { width: 100%; height: 100vh; }
</style>
</head>
<body>
<div id="graph-container"></div>
<!-- Load D3.js from CDN -->
<script src="https://d3js.org/d3.v7.min.js"></script>
<!-- Load the application bundle -->
<script type="module" src="../src/index.js"></script>
</body>
</html>
+1 -2
View File
@@ -1,4 +1,3 @@
langgraph langgraph
langchain-openai langchain-openai
langchain-core openai
python-dotenv
+1
View File
@@ -0,0 +1 @@
# src package initialization
+66
View File
@@ -0,0 +1,66 @@
// src/graph.js
// A simple graph implementation with reflexive edge support
// This module is pure JavaScript and can be used in both Node and browser environments.
export class Graph {
constructor() {
// adjacency list: nodeId -> Set of neighbor nodeIds
this.adj = new Map();
// node properties: nodeId -> {label, ...}
this.nodes = new Map();
}
// Add a node with optional properties
addNode(id, props = {}) {
if (this.nodes.has(id)) {
throw new Error(`Node ${id} already exists`);
}
this.nodes.set(id, { id, ...props });
this.adj.set(id, new Set());
}
// Add a directed edge from src to dst
addEdge(src, dst) {
if (!this.nodes.has(src) || !this.nodes.has(dst)) {
throw new Error(`Both nodes must exist to add an edge: ${src} -> ${dst}`);
}
this.adj.get(src).add(dst);
}
// Return array of neighbor ids for a node
neighbors(id) {
if (!this.adj.has(id)) return [];
return Array.from(this.adj.get(id));
}
// Add reflexive edges (self-loops) to all nodes
addReflexiveEdges() {
for (const id of this.nodes.keys()) {
this.adj.get(id).add(id);
}
}
// Return a plain object representation (useful for serialization)
toJSON() {
const nodes = Array.from(this.nodes.values());
const edges = [];
for (const [src, dstSet] of this.adj.entries()) {
for (const dst of dstSet) {
edges.push({ src, dst });
}
}
return { nodes, edges };
}
// Static helper to create a graph from a JSON representation
static fromJSON(json) {
const g = new Graph();
for (const node of json.nodes) {
g.addNode(node.id, node);
}
for (const edge of json.edges) {
g.addEdge(edge.src, edge.dst);
}
return g;
}
}
+76 -20
View File
@@ -1,24 +1,80 @@
from langgraph.graph import StateGraph, END, START from langgraph import Graph, State
from src.state import CodeReviewState import inspect
from src.nodes import draft_review, reflect, rewrite import os
from typing import Any, Dict
def build_graph() -> StateGraph: class MyState(State):
graph = StateGraph(CodeReviewState) """
State for the graph. Holds the query string and the result.
"""
query: str
result: str = ""
# Add nodes def get_source_of_file(file_path: str) -> str:
graph.add_node("draft_review", draft_review) """
graph.add_node("reflect", reflect) Reads the source code of a file.
graph.add_node("rewrite", rewrite)
# Define transitions Args:
graph.set_entry_point("draft_review") file_path: Path to the file relative to this module.
graph.add_edge("draft_review", "reflect")
graph.add_conditional_edges(
"reflect",
lambda state: (
"rewrite" if state["verdict"] == "needs_revision" and state["round"] < state["max_rounds"] else END
),
)
graph.add_edge("rewrite", "reflect")
return graph Returns:
The file contents or an error message if the file does not exist.
"""
if not os.path.isfile(file_path):
return f"File not found: {file_path}"
try:
with open(file_path, "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
return f"Error reading {file_path}: {e}"
def get_source_of_object(obj_name: str) -> str:
"""
Retrieves the source code of a Python object (function, class, etc.) by name.
Args:
obj_name: Name of the object defined in this module.
Returns:
The source code string or an error message if the object is not found.
"""
obj = globals().get(obj_name)
if obj is None:
return f"Object not found: {obj_name}"
try:
return inspect.getsource(obj)
except Exception as e:
return f"Could not retrieve source for {obj_name}: {e}"
def reflect(state: MyState) -> MyState:
"""
Node that processes a query asking for source code.
Supported queries:
- "source of <file.py>"
- "source of <function_name>"
The result is stored in state.result.
"""
query = state.query.strip()
if query.lower().startswith("source of "):
target = query[10:].strip()
if target.endswith(".py"):
# Resolve file path relative to this module
file_path = os.path.join(os.path.dirname(__file__), target)
source = get_source_of_file(file_path)
else:
source = get_source_of_object(target)
state.result = source
else:
state.result = (
"Unsupported query. Please use 'source of <file.py>' "
"or 'source of <function_name>'."
)
return state
# Build the LangGraph graph
graph = Graph()
graph.add_node("reflect", reflect)
graph.set_entry_point("reflect")
graph.set_finish("reflect")
+24
View File
@@ -0,0 +1,24 @@
// src/index.js
// Entry point for the application
import { Graph } from './graph.js';
import { renderGraph } from './ui.js';
document.addEventListener('DOMContentLoaded', () => {
// Create a sample graph
const g = new Graph();
g.addNode('A', { label: 'Node A' });
g.addNode('B', { label: 'Node B' });
g.addNode('C', { label: 'Node C' });
g.addNode('D', { label: 'Node D' });
g.addEdge('A', 'B');
g.addEdge('B', 'C');
g.addEdge('C', 'D');
g.addEdge('D', 'A');
// Add reflexive edges (self-loops)
g.addReflexiveEdges();
// Render the graph into the container with id "graph-container"
renderGraph(g, 'graph-container');
});
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""
Graph implementation using an adjacency list.
This module defines a simple undirected graph data structure that
supports adding and removing nodes and edges, querying adjacency,
and iterating over nodes and edges. The implementation uses a
single approach an adjacency dictionary and does not mix
alternative representations.
Author: Artur Kuzakhmetov
"""
from __future__ import annotations
from collections import defaultdict
from typing import Dict, Iterable, List, Set, Tuple
class Graph:
"""
Undirected graph represented by an adjacency list.
Nodes can be any hashable Python object. Edges are stored
as unordered pairs; selfloops (reflexive edges) are allowed.
"""
def __init__(self, nodes: Iterable = None, edges: Iterable[Tuple] = None):
"""
Create a new graph.
Parameters
----------
nodes : Iterable, optional
Iterable of initial nodes.
edges : Iterable[Tuple], optional
Iterable of initial edges, each edge is a tuple
(node1, node2). For selfloops, node1 == node2.
"""
self._adj: Dict = defaultdict(set) # type: Dict[object, Set[object]]
if nodes:
for node in nodes:
self.add_node(node)
if edges:
for n1, n2 in edges:
self.add_edge(n1, n2)
# ------------------------------------------------------------------
# Node operations
# ------------------------------------------------------------------
def add_node(self, node: object) -> None:
"""Add a node to the graph. If the node already exists, do nothing."""
self._adj.setdefault(node, set())
def remove_node(self, node: object) -> None:
"""Remove a node and all incident edges."""
if node not in self._adj:
raise KeyError(f"Node {node!r} not found")
# Remove node from neighbors' adjacency sets
for neighbor in list(self._adj[node]):
self._adj[neighbor].discard(node)
# Remove the node itself
del self._adj[node]
def nodes(self) -> Set[object]:
"""Return a set of all nodes in the graph."""
return set(self._adj.keys())
# ------------------------------------------------------------------
# Edge operations
# ------------------------------------------------------------------
def add_edge(self, n1: object, n2: object) -> None:
"""
Add an undirected edge between n1 and n2.
If either node does not exist, it is created automatically.
"""
self.add_node(n1)
self.add_node(n2)
self._adj[n1].add(n2)
self._adj[n2].add(n1)
def remove_edge(self, n1: object, n2: object) -> None:
"""Remove the edge between n1 and n2. Raises KeyError if not present."""
if n1 not in self._adj or n2 not in self._adj:
raise KeyError("One or both nodes not found")
if n2 not in self._adj[n1]:
raise KeyError(f"Edge ({n1!r}, {n2!r}) does not exist")
self._adj[n1].discard(n2)
self._adj[n2].discard(n1)
def has_edge(self, n1: object, n2: object) -> bool:
"""Return True if an edge exists between n1 and n2."""
return n1 in self._adj and n2 in self._adj[n1]
def edges(self) -> Set[Tuple[object, object]]:
"""Return a set of all edges as unordered tuples."""
seen = set()
for n, neighbors in self._adj.items():
for m in neighbors:
if (m, n) not in seen:
seen.add((n, m))
return seen
# ------------------------------------------------------------------
# Adjacency queries
# ------------------------------------------------------------------
def neighbors(self, node: object) -> Set[object]:
"""Return the set of neighbors of the given node."""
if node not in self._adj:
raise KeyError(f"Node {node!r} not found")
return set(self._adj[node])
# ------------------------------------------------------------------
# Utility methods
# ------------------------------------------------------------------
def __len__(self) -> int:
"""Return the number of nodes in the graph."""
return len(self._adj)
def __repr__(self) -> str:
return f"Graph(nodes={list(self._adj.keys())}, edges={list(self.edges())})"
def __str__(self) -> str:
lines = [f"Graph with {len(self)} nodes and {len(self.edges())} edges:"]
for node in sorted(self._adj):
neigh = ", ".join(map(str, sorted(self._adj[node])))
lines.append(f" {node}: {neigh}")
return "\n".join(lines)
# ----------------------------------------------------------------------
# Example usage
# ----------------------------------------------------------------------
if __name__ == "__main__":
g = Graph()
g.add_edge("A", "B")
g.add_edge("B", "C")
g.add_edge("C", "A") # triangle
g.add_edge("D", "D") # reflexive edge
print(g)
print("Neighbors of B:", g.neighbors("B"))
print("Has edge (A, D)?", g.has_edge("A", "D"))
g.remove_edge("A", "B")
print("After removing edge (A, B):")
print(g)
g.remove_node("C")
print("After removing node C:")
print(g)
+103 -2
View File
@@ -1,4 +1,105 @@
from src.cli import main #!/usr/bin/env python3
"""
Graph with reflection on code using LangGraph.
This script defines a simple LangGraph workflow that takes a code snippet,
passes it to an LLM for reflection, and outputs the reflection.
Requirements:
- langgraph
- langchain-openai
- openai
Set the environment variable OPENAI_API_KEY with your OpenAI API key.
"""
import os
from typing import Dict, Any
from langgraph.graph import StateGraph, END
from langchain_openai import OpenAI
# Define the state type for the graph
class CodeState(dict):
"""
State dictionary that holds the code snippet and the reflection.
"""
pass
def input_node(state: CodeState) -> Dict[str, Any]:
"""
Entry node that simply passes the code snippet through.
"""
# The state is expected to contain a 'code' key.
return {"code": state.get("code", "")}
def reflection_node(state: CodeState) -> Dict[str, Any]:
"""
Node that uses an LLM to generate a reflection on the provided code.
"""
code = state.get("code", "")
if not code:
return {"reflection": "No code provided."}
# Initialize the LLM
llm = OpenAI(temperature=0.7, model="gpt-3.5-turbo")
# Prompt the LLM to analyze the code and provide reflection
prompt = (
"You are an experienced software engineer. "
"Analyze the following code snippet and provide a concise reflection "
"on its structure, potential improvements, and any notable patterns.\n\n"
f"{code}"
)
# Invoke the LLM
response = llm.invoke(prompt)
# The response is a string; store it in the state
return {"reflection": response}
def output_node(state: CodeState) -> Dict[str, Any]:
"""
Final node that simply returns the reflection.
"""
return {"reflection": state.get("reflection", "")}
# Build the graph
graph = StateGraph(CodeState)
# Add nodes
graph.add_node("input", input_node)
graph.add_node("reflection", reflection_node)
graph.add_node("output", output_node)
# Define edges
graph.add_edge("input", "reflection")
graph.add_edge("reflection", "output")
graph.add_edge("output", END)
# Compile the graph into an executable app
app = graph.compile()
def run_graph(code_snippet: str) -> str:
"""
Run the graph with the provided code snippet and return the reflection.
"""
# Prepare the initial state
initial_state = {"code": code_snippet}
# Invoke the graph
result = app.invoke(initial_state)
# Extract the reflection
return result.get("reflection", "")
if __name__ == "__main__": if __name__ == "__main__":
main() # Example usage
sample_code = """
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)
"""
reflection = run_graph(sample_code)
print("Reflection on code:")
print(reflection)
+100
View File
@@ -0,0 +1,100 @@
// src/ui.js
// Simple UI rendering using D3.js
// Assumes D3 is loaded globally (e.g., via CDN in index.html)
export function renderGraph(graph, containerId) {
const container = document.getElementById(containerId);
if (!container) {
throw new Error(`Container with id "${containerId}" not found`);
}
// Clear previous content
container.innerHTML = '';
const width = container.clientWidth || 600;
const height = container.clientHeight || 400;
const svg = d3.select(container)
.append('svg')
.attr('width', width)
.attr('height', height);
const nodes = graph.nodes.values();
const edges = [];
for (const [src, dstSet] of graph.adj.entries()) {
for (const dst of dstSet) {
edges.push({ source: src, target: dst });
}
}
// Simple force simulation for layout
const simulation = d3.forceSimulation(Array.from(nodes))
.force('link', d3.forceLink(edges).id(d => d.id).distance(120))
.force('charge', d3.forceManyBody().strength(-300))
.force('center', d3.forceCenter(width / 2, height / 2));
const link = svg.append('g')
.attr('class', 'links')
.selectAll('line')
.data(edges)
.enter()
.append('line')
.attr('stroke', '#999')
.attr('stroke-width', 1.5);
const node = svg.append('g')
.attr('class', 'nodes')
.selectAll('circle')
.data(Array.from(nodes))
.enter()
.append('circle')
.attr('r', 20)
.attr('fill', '#69b3a2')
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended));
const label = svg.append('g')
.attr('class', 'labels')
.selectAll('text')
.data(Array.from(nodes))
.enter()
.append('text')
.attr('dy', 4)
.attr('text-anchor', 'middle')
.text(d => d.label || d.id);
simulation.on('tick', () => {
link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
node
.attr('cx', d => d.x)
.attr('cy', d => d.y);
label
.attr('x', d => d.x)
.attr('y', d => d.y);
});
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
}
+87
View File
@@ -0,0 +1,87 @@
const { createGraph } = require('../src/index');
describe('Graph with reflection', () => {
let graph;
beforeEach(() => {
graph = createGraph();
});
test('initial log is empty', () => {
expect(graph.getLog()).toEqual([]);
});
test('addNode records operation', () => {
graph.addNode('a');
expect(graph.getLog()).toEqual([{ method: 'addNode', args: ['a'] }]);
expect(graph.getNodes()).toEqual(['a']);
});
test('addEdge records operation and creates nodes', () => {
graph.addEdge('a', 'b');
expect(graph.getLog()).toEqual([{ method: 'addEdge', args: ['a', 'b'] }]);
expect(graph.getNodes().sort()).toEqual(['a', 'b']);
expect(graph.getNeighbors('a')).toEqual(['b']);
expect(graph.getNeighbors('b')).toEqual(['a']);
expect(graph.hasEdge('a', 'b')).toBe(true);
});
test('removeEdge records operation', () => {
graph.addEdge('a', 'b');
graph.removeEdge('a', 'b');
expect(graph.getLog()).toEqual([
{ method: 'addEdge', args: ['a', 'b'] },
{ method: 'removeEdge', args: ['a', 'b'] }
]);
expect(graph.hasEdge('a', 'b')).toBe(false);
});
test('removeNode records operation and removes edges', () => {
graph.addEdge('a', 'b');
graph.addEdge('a', 'c');
graph.removeNode('a');
expect(graph.getLog()).toEqual([
{ method: 'addEdge', args: ['a', 'b'] },
{ method: 'addEdge', args: ['a', 'c'] },
{ method: 'removeNode', args: ['a'] }
]);
expect(graph.getNodes().sort()).toEqual(['b', 'c']);
expect(graph.getNeighbors('b')).toEqual([]);
expect(graph.getNeighbors('c')).toEqual([]);
});
test('getNeighbors does not record operation', () => {
graph.addNode('a');
graph.getNeighbors('a');
expect(graph.getLog()).toEqual([{ method: 'addNode', args: ['a'] }]);
});
test('log is a copy and not affected by external mutation', () => {
graph.addNode('a');
const log = graph.getLog();
log.push({ method: 'fake', args: [] });
expect(graph.getLog()).toEqual([{ method: 'addNode', args: ['a'] }]);
});
test('graph uses adjacency list internally', () => {
graph.addNode('a');
expect(graph.adj instanceof Map).toBe(true);
expect(graph.adj.get('a') instanceof Set).toBe(true);
});
test('handles duplicate nodes and edges gracefully', () => {
graph.addNode('a');
graph.addNode('a');
expect(graph.getNodes()).toEqual(['a']);
graph.addEdge('a', 'a');
expect(graph.hasEdge('a', 'a')).toBe(true);
graph.addEdge('a', 'a');
expect(graph.getNeighbors('a')).toEqual(['a']);
});
test('handles non-existing nodes and edges', () => {
expect(graph.getNeighbors('x')).toEqual([]);
expect(graph.hasEdge('x', 'y')).toBe(false);
graph.removeEdge('x', 'y'); // should not throw
graph.removeNode('x'); // should not throw
});
});
+55
View File
@@ -0,0 +1,55 @@
const Graph = require('../src/index');
describe('Graph with reflexivity', () => {
let g;
beforeEach(() => {
g = new Graph();
});
test('adding a node creates reflexive edge', () => {
g.addNode('A');
expect(g.nodes()).toContain('A');
expect(g.hasEdge('A', 'A')).toBe(true);
});
test('adding an edge between existing nodes', () => {
g.addNode('A');
g.addNode('B');
g.addEdge('A', 'B');
expect(g.hasEdge('A', 'B')).toBe(true);
expect(g.hasEdge('B', 'A')).toBe(false);
});
test('adding an edge automatically adds missing nodes', () => {
g.addEdge('X', 'Y');
expect(g.nodes()).toEqual(expect.arrayContaining(['X', 'Y']));
expect(g.hasEdge('X', 'Y')).toBe(true);
// reflexive edges for both nodes
expect(g.hasEdge('X', 'X')).toBe(true);
expect(g.hasEdge('Y', 'Y')).toBe(true);
});
test('getNeighbors returns correct neighbors', () => {
g.addNode('1');
g.addNode('2');
g.addEdge('1', '2');
expect(g.getNeighbors('1')).toEqual(expect.arrayContaining(['1', '2']));
expect(g.getNeighbors('2')).toEqual(['2']);
});
test('edges method returns all edges', () => {
g.addNode('A');
g.addNode('B');
g.addEdge('A', 'B');
const edges = g.edges();
expect(edges).toEqual(
expect.arrayContaining([
['A', 'A'],
['B', 'B'],
['A', 'B']
])
);
expect(edges.length).toBe(3);
});
});