From 92ef84b1e4843e7cd2039e4ff33318e0efc6311b Mon Sep 17 00:00:00 2001 From: mochan Date: Mon, 27 Apr 2026 18:18:09 +0530 Subject: [PATCH 1/7] test: cover viz._style + review.differ kind_str helpers --- tests/test_style_helpers.py | 52 +++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tests/test_style_helpers.py diff --git a/tests/test_style_helpers.py b/tests/test_style_helpers.py new file mode 100644 index 0000000..f689d7f --- /dev/null +++ b/tests/test_style_helpers.py @@ -0,0 +1,52 @@ +"""Direct unit tests for kind_str helpers in viz._style and review.differ.""" +from __future__ import annotations + +from enum import Enum + +from codegraph.graph.schema import EdgeKind, NodeKind +from codegraph.review import differ +from codegraph.viz._style import kind_str + + +class _DummyEnum(Enum): + FOO = "FOO_VAL" + + +def test_kind_str_with_enum_returns_value() -> None: + assert kind_str(NodeKind.FUNCTION) == "FUNCTION" + + +def test_kind_str_with_string_returns_string() -> None: + assert kind_str("CLASS") == "CLASS" + + +def test_kind_str_with_none_returns_empty_string() -> None: + assert kind_str(None) == "" + + +def test_kind_str_with_arbitrary_enum_returns_value() -> None: + assert kind_str(_DummyEnum.FOO) == "FOO_VAL" + + +def test_kind_str_with_int_returns_str() -> None: + assert kind_str(42) == "42" + + +def test_kind_str_with_empty_string() -> None: + assert kind_str("") == "" + + +def test_differ_kind_str_with_edge_enum() -> None: + assert differ._kind_str(EdgeKind.CALLS) == "CALLS" + + +def test_differ_kind_str_with_string_passthrough() -> None: + assert differ._kind_str("DEFINED_IN") == "DEFINED_IN" + + +def test_differ_kind_str_with_none() -> None: + assert differ._kind_str(None) == "" + + +def test_differ_kind_str_with_object_without_value_attr() -> None: + assert differ._kind_str(123) == "123" From 90a1e81603014a354c8120b999622f32f02a335f Mon Sep 17 00:00:00 2001 From: mochan Date: Mon, 27 Apr 2026 18:18:19 +0530 Subject: [PATCH 2/7] test: cover analysis.roles helpers (_set_role, _decorators) --- tests/test_roles_helpers.py | 86 +++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 tests/test_roles_helpers.py diff --git a/tests/test_roles_helpers.py b/tests/test_roles_helpers.py new file mode 100644 index 0000000..383ac83 --- /dev/null +++ b/tests/test_roles_helpers.py @@ -0,0 +1,86 @@ +"""Direct unit tests for roles helper functions (_set_role, _decorators).""" +from __future__ import annotations + +import networkx as nx + +from codegraph.analysis.roles import ( + COMPONENT, + HANDLER, + REPO, + SERVICE, + _decorators, + _set_role, +) + + +def test_decorators_returns_string_list() -> None: + attrs = {"metadata": {"decorators": ["@app.get('/')", "@cached"]}} + assert _decorators(attrs) == ["@app.get('/')", "@cached"] + + +def test_decorators_handles_missing_metadata() -> None: + assert _decorators({}) == [] + + +def test_decorators_handles_none_metadata() -> None: + assert _decorators({"metadata": None}) == [] + + +def test_decorators_handles_non_dict_metadata() -> None: + assert _decorators({"metadata": "not a dict"}) == [] + + +def test_decorators_handles_non_list_decorators() -> None: + assert _decorators({"metadata": {"decorators": "x"}}) == [] + + +def test_decorators_coerces_items_to_str() -> None: + attrs = {"metadata": {"decorators": [1, 2, "three"]}} + assert _decorators(attrs) == ["1", "2", "three"] + + +def test_set_role_assigns_when_no_existing_role() -> None: + g = nx.MultiDiGraph() + g.add_node("n1", metadata={}) + assert _set_role(g, "n1", SERVICE) is True + assert g.nodes["n1"]["metadata"]["role"] == SERVICE + + +def test_set_role_creates_metadata_dict_when_missing() -> None: + g = nx.MultiDiGraph() + g.add_node("n1") + assert _set_role(g, "n1", REPO) is True + assert g.nodes["n1"]["metadata"]["role"] == REPO + + +def test_set_role_replaces_metadata_when_not_dict() -> None: + g = nx.MultiDiGraph() + g.add_node("n1", metadata="bogus") + assert _set_role(g, "n1", REPO) is True + assert g.nodes["n1"]["metadata"] == {"role": REPO} + + +def test_set_role_respects_priority_higher_wins() -> None: + g = nx.MultiDiGraph() + g.add_node("n1", metadata={"role": SERVICE}) + # HANDLER has higher priority than SERVICE + assert _set_role(g, "n1", HANDLER) is True + assert g.nodes["n1"]["metadata"]["role"] == HANDLER + + +def test_set_role_rejects_lower_priority() -> None: + g = nx.MultiDiGraph() + g.add_node("n1", metadata={"role": HANDLER}) + assert _set_role(g, "n1", SERVICE) is False + assert g.nodes["n1"]["metadata"]["role"] == HANDLER + + +def test_set_role_rejects_equal_priority() -> None: + g = nx.MultiDiGraph() + g.add_node("n1", metadata={"role": COMPONENT}) + assert _set_role(g, "n1", COMPONENT) is False + + +def test_set_role_returns_false_for_missing_node() -> None: + g = nx.MultiDiGraph() + assert _set_role(g, "ghost", SERVICE) is False From 098eae9b04b5b6d6f3bab1eb50b8873e12ec0c41 Mon Sep 17 00:00:00 2001 From: mochan Date: Mon, 27 Apr 2026 18:18:19 +0530 Subject: [PATCH 3/7] test: cover parsers.typescript helpers (strip_quotes, object_keys, params, return_type) --- tests/test_typescript_helpers.py | 191 +++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 tests/test_typescript_helpers.py diff --git a/tests/test_typescript_helpers.py b/tests/test_typescript_helpers.py new file mode 100644 index 0000000..29d820a --- /dev/null +++ b/tests/test_typescript_helpers.py @@ -0,0 +1,191 @@ +"""Direct unit tests for codegraph.parsers.typescript helpers.""" +from __future__ import annotations + +import tree_sitter + +from codegraph.parsers.base import load_parser +from codegraph.parsers.typescript import ( + _extract_params, + _extract_return_type, + _object_top_level_keys, + _strip_quotes, +) + +# --------------------------------------------------------------------------- +# _strip_quotes — pure string helper +# --------------------------------------------------------------------------- + + +def test_strip_quotes_double() -> None: + assert _strip_quotes('"hello"') == "hello" + + +def test_strip_quotes_single() -> None: + assert _strip_quotes("'world'") == "world" + + +def test_strip_quotes_backtick() -> None: + assert _strip_quotes("`tmpl`") == "tmpl" + + +def test_strip_quotes_unquoted_passthrough() -> None: + assert _strip_quotes("plain") == "plain" + + +def test_strip_quotes_mismatched_left_only() -> None: + assert _strip_quotes('"abc') == '"abc' + + +def test_strip_quotes_mismatched_pair() -> None: + # Opening " but closing ' — mismatch, no strip + assert _strip_quotes("\"abc'") == "\"abc'" + + +def test_strip_quotes_empty_string() -> None: + assert _strip_quotes("") == "" + + +def test_strip_quotes_single_char() -> None: + assert _strip_quotes('"') == '"' + + +# --------------------------------------------------------------------------- +# Tree-sitter dependent helpers — use a real TS parser +# --------------------------------------------------------------------------- + + +def _parse_ts(source: str) -> tuple[tree_sitter.Tree, bytes]: + parser = load_parser("typescript") + src_bytes = source.encode("utf-8") + return parser.parse(src_bytes), src_bytes + + +def _find_first(node: tree_sitter.Node, type_name: str) -> tree_sitter.Node | None: + if node.type == type_name: + return node + for c in node.children: + found = _find_first(c, type_name) + if found is not None: + return found + return None + + +def test_object_top_level_keys_basic() -> None: + tree, src = _parse_ts("const x = { a: 1, b: 2, c: 3 };") + obj = _find_first(tree.root_node, "object") + assert obj is not None + keys = _object_top_level_keys(obj, src) + assert keys == ["a", "b", "c"] + + +def test_object_top_level_keys_string_keys_stripped() -> None: + tree, src = _parse_ts('const x = { "a": 1, "b": 2 };') + obj = _find_first(tree.root_node, "object") + assert obj is not None + keys = _object_top_level_keys(obj, src) + assert keys == ["a", "b"] + + +def test_object_top_level_keys_shorthand() -> None: + tree, src = _parse_ts("const a = 1; const b = 2; const x = { a, b };") + # Find the *last* object node — first may be other constructs. + objs: list[tree_sitter.Node] = [] + + def walk(n: tree_sitter.Node) -> None: + if n.type == "object": + objs.append(n) + for c in n.children: + walk(c) + + walk(tree.root_node) + assert objs + keys = _object_top_level_keys(objs[-1], src) + assert "a" in keys and "b" in keys + + +def test_object_top_level_keys_non_object_returns_empty() -> None: + tree, src = _parse_ts("const x = 42;") + # Pass the entire root_node — _object_top_level_keys checks type=="object". + keys = _object_top_level_keys(tree.root_node, src) + assert keys == [] + + +def test_object_top_level_keys_empty_object() -> None: + tree, src = _parse_ts("const x = {};") + obj = _find_first(tree.root_node, "object") + assert obj is not None + assert _object_top_level_keys(obj, src) == [] + + +# --------------------------------------------------------------------------- +# _extract_params +# --------------------------------------------------------------------------- + + +def _find_function(tree: tree_sitter.Tree) -> tree_sitter.Node | None: + return _find_first(tree.root_node, "function_declaration") + + +def test_extract_params_none_node() -> None: + assert _extract_params(None, b"") == [] + + +def test_extract_params_simple_typed() -> None: + tree, src = _parse_ts("function f(a: number, b: string): void {}") + fn = _find_function(tree) + assert fn is not None + params_node = fn.child_by_field_name("parameters") + out = _extract_params(params_node, src) + names = [p["name"] for p in out] + assert "a" in names and "b" in names + + +def test_extract_params_no_params() -> None: + tree, src = _parse_ts("function f(): void {}") + fn = _find_function(tree) + assert fn is not None + params_node = fn.child_by_field_name("parameters") + assert _extract_params(params_node, src) == [] + + +def test_extract_params_with_default() -> None: + tree, src = _parse_ts("function f(a: number = 5): void {}") + fn = _find_function(tree) + assert fn is not None + params_node = fn.child_by_field_name("parameters") + out = _extract_params(params_node, src) + assert len(out) == 1 + assert out[0]["name"] == "a" + + +# --------------------------------------------------------------------------- +# _extract_return_type +# --------------------------------------------------------------------------- + + +def test_extract_return_type_explicit() -> None: + tree, src = _parse_ts("function f(a: number): string { return ''; }") + fn = _find_function(tree) + assert fn is not None + params_node = fn.child_by_field_name("parameters") + rt = _extract_return_type(fn, params_node, src) + assert rt == "string" + + +def test_extract_return_type_missing() -> None: + tree, src = _parse_ts("function f(a: number) { return 1; }") + fn = _find_function(tree) + assert fn is not None + params_node = fn.child_by_field_name("parameters") + rt = _extract_return_type(fn, params_node, src) + assert rt is None + + +def test_extract_return_type_complex_generic() -> None: + tree, src = _parse_ts("function f(): Promise { return Promise.resolve(1); }") + fn = _find_function(tree) + assert fn is not None + params_node = fn.child_by_field_name("parameters") + rt = _extract_return_type(fn, params_node, src) + assert rt is not None + assert "Promise" in rt From cc52bfa714d3d499f1fc7cb27d2018721eb1bc5a Mon Sep 17 00:00:00 2001 From: mochan Date: Mon, 27 Apr 2026 18:18:19 +0530 Subject: [PATCH 4/7] test: cover mcp_server._node_df_metadata --- tests/test_mcp_helpers.py | 50 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/test_mcp_helpers.py diff --git a/tests/test_mcp_helpers.py b/tests/test_mcp_helpers.py new file mode 100644 index 0000000..fb10c6e --- /dev/null +++ b/tests/test_mcp_helpers.py @@ -0,0 +1,50 @@ +"""Direct unit tests for codegraph.mcp_server.server._node_df_metadata.""" +from __future__ import annotations + +from codegraph.mcp_server.server import _node_df_metadata + + +def test_returns_empty_when_no_metadata() -> None: + assert _node_df_metadata({}) == {} + + +def test_returns_empty_when_metadata_none() -> None: + assert _node_df_metadata({"metadata": None}) == {} + + +def test_returns_empty_when_metadata_not_dict() -> None: + assert _node_df_metadata({"metadata": "bogus"}) == {} + + +def test_extracts_params() -> None: + out = _node_df_metadata({"metadata": {"params": ["a", "b"]}}) + assert out == {"params": ["a", "b"]} + + +def test_extracts_returns() -> None: + out = _node_df_metadata({"metadata": {"returns": "int"}}) + assert out == {"returns": "int"} + + +def test_extracts_role() -> None: + out = _node_df_metadata({"metadata": {"role": "HANDLER"}}) + assert out == {"role": "HANDLER"} + + +def test_omits_role_when_none() -> None: + out = _node_df_metadata({"metadata": {"role": None}}) + assert "role" not in out + + +def test_extracts_all_fields_together() -> None: + out = _node_df_metadata( + {"metadata": {"params": ["a"], "returns": "str", "role": "SERVICE"}} + ) + assert out == {"params": ["a"], "returns": "str", "role": "SERVICE"} + + +def test_unrelated_keys_are_ignored() -> None: + out = _node_df_metadata( + {"metadata": {"params": ["a"], "extra": "ignored"}} + ) + assert out == {"params": ["a"]} From f82e4f8870744ba2f03ed5d57e03b7a77f78dd1b Mon Sep 17 00:00:00 2001 From: mochan Date: Mon, 27 Apr 2026 18:18:19 +0530 Subject: [PATCH 5/7] test: cover viz.diagrams + viz.hld helpers --- tests/test_diagrams_helpers.py | 92 ++++++++++++++++++++++++++++++++++ tests/test_hld_helpers.py | 49 ++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 tests/test_diagrams_helpers.py create mode 100644 tests/test_hld_helpers.py diff --git a/tests/test_diagrams_helpers.py b/tests/test_diagrams_helpers.py new file mode 100644 index 0000000..9e886e7 --- /dev/null +++ b/tests/test_diagrams_helpers.py @@ -0,0 +1,92 @@ +"""Direct unit tests for codegraph.viz.diagrams._module_index.""" +from __future__ import annotations + +import networkx as nx + +from codegraph.graph.schema import NodeKind +from codegraph.viz.diagrams import _module_index + + +def _make_graph() -> nx.MultiDiGraph: + g = nx.MultiDiGraph() + g.add_node( + "m1", + kind=NodeKind.MODULE, + qualname="pkg.alpha", + name="alpha", + file="pkg/alpha.py", + language="python", + metadata={"is_test": False}, + ) + g.add_node( + "f1", + kind=NodeKind.FUNCTION, + qualname="pkg.alpha.foo", + name="foo", + file="pkg/alpha.py", + line_start=1, + line_end=10, + ) + g.add_node( + "c1", + kind=NodeKind.CLASS, + qualname="pkg.alpha.Foo", + name="Foo", + file="pkg/alpha.py", + line_start=11, + line_end=20, + ) + return g + + +def test_module_index_maps_module_to_itself() -> None: + g = _make_graph() + node_to_module, _info = _module_index(g) + assert node_to_module["m1"] == "m1" + + +def test_module_index_maps_symbols_to_module_by_file() -> None: + g = _make_graph() + node_to_module, _ = _module_index(g) + assert node_to_module["f1"] == "m1" + assert node_to_module["c1"] == "m1" + + +def test_module_index_loc_is_max_line_end() -> None: + g = _make_graph() + _, info = _module_index(g) + assert info["m1"]["loc"] == 20 + + +def test_module_index_counts_symbols() -> None: + g = _make_graph() + _, info = _module_index(g) + assert info["m1"]["symbols"] == 2 + + +def test_module_index_extracts_package() -> None: + g = _make_graph() + _, info = _module_index(g) + assert info["m1"]["package"] == "pkg" + + +def test_module_index_empty_graph() -> None: + g = nx.MultiDiGraph() + node_to_module, info = _module_index(g) + assert node_to_module == {} + assert info == {} + + +def test_module_index_skips_orphan_symbol_without_matching_file() -> None: + g = _make_graph() + g.add_node( + "orphan", + kind=NodeKind.FUNCTION, + qualname="other.bar", + name="bar", + file="other/bar.py", + line_start=1, + line_end=5, + ) + node_to_module, _ = _module_index(g) + assert "orphan" not in node_to_module diff --git a/tests/test_hld_helpers.py b/tests/test_hld_helpers.py new file mode 100644 index 0000000..3d9fb80 --- /dev/null +++ b/tests/test_hld_helpers.py @@ -0,0 +1,49 @@ +"""Direct unit tests for codegraph.viz.hld helpers.""" +from __future__ import annotations + +from codegraph.viz.hld import _is_skippable_module, _short_name + + +def test_short_name_simple() -> None: + assert _short_name("pkg.module.foo") == "foo" + + +def test_short_name_no_dots() -> None: + assert _short_name("foo") == "foo" + + +def test_short_name_empty_string() -> None: + assert _short_name("") == "" + + +def test_short_name_trailing_dot() -> None: + # rsplit on trailing dot gives empty string + assert _short_name("pkg.module.") == "" + + +def test_short_name_single_segment() -> None: + assert _short_name("alpha") == "alpha" + + +def test_is_skippable_module_test_dir() -> None: + assert _is_skippable_module("pkg.tests.foo", "pkg/tests/foo.py") is True + + +def test_is_skippable_module_init_py() -> None: + assert _is_skippable_module("pkg", "pkg/__init__.py") is True + + +def test_is_skippable_module_regular() -> None: + assert _is_skippable_module("pkg.module", "pkg/module.py") is False + + +def test_is_skippable_module_test_prefix_segment() -> None: + assert _is_skippable_module("pkg.test_utils", "pkg/test_utils.py") is True + + +def test_is_skippable_module_top_init() -> None: + assert _is_skippable_module("pkg", "__init__.py") is True + + +def test_is_skippable_module_windows_path_init() -> None: + assert _is_skippable_module("pkg", "pkg\\__init__.py") is True From b63dd005ea352e3d70d55ea7d2eea54a47ae1904 Mon Sep 17 00:00:00 2001 From: mochan Date: Mon, 27 Apr 2026 18:18:19 +0530 Subject: [PATCH 6/7] test: cover embed.store + embed.embedder helpers --- tests/test_embed_helpers.py | 166 ++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 tests/test_embed_helpers.py diff --git a/tests/test_embed_helpers.py b/tests/test_embed_helpers.py new file mode 100644 index 0000000..ce1533d --- /dev/null +++ b/tests/test_embed_helpers.py @@ -0,0 +1,166 @@ +"""Direct unit tests for codegraph.embed.store._JsonBackend and embedder. + +The Embedder.embed method is the public encoding entry point — exercise it +with a deterministic in-memory fake encoder. +""" +from __future__ import annotations + +import hashlib +import math +from pathlib import Path +from typing import Any + +from codegraph.embed.embedder import Embedder +from codegraph.embed.store import StoredChunk, _JsonBackend + +# --------------------------------------------------------------------------- +# JsonBackend tests +# --------------------------------------------------------------------------- + +def _row(rid: str, vec: list[float] | None = None) -> StoredChunk: + return StoredChunk( + id=rid, + qualname=f"pkg.{rid}", + file=f"{rid}.py", + line_start=1, + line_end=5, + kind="FUNCTION", + role=None, + text=f"def {rid}(): pass", + vector=vec or [1.0, 0.0], + ) + + +def test_json_backend_upsert_and_all(tmp_path: Path) -> None: + backend = _JsonBackend(tmp_path / "x.json") + backend.upsert([_row("a"), _row("b")]) + out = backend.all() + assert {r.id for r in out} == {"a", "b"} + + +def test_json_backend_upsert_replaces_existing_id(tmp_path: Path) -> None: + backend = _JsonBackend(tmp_path / "x.json") + backend.upsert([_row("a", [1.0, 0.0])]) + backend.upsert([_row("a", [0.0, 1.0])]) + rows = backend.all() + assert len(rows) == 1 + assert rows[0].vector == [0.0, 1.0] + + +def test_json_backend_replace_all_clears_old(tmp_path: Path) -> None: + backend = _JsonBackend(tmp_path / "x.json") + backend.upsert([_row("a"), _row("b")]) + backend.replace_all([_row("c")]) + ids = {r.id for r in backend.all()} + assert ids == {"c"} + + +def test_json_backend_query_returns_top_k_sorted(tmp_path: Path) -> None: + backend = _JsonBackend(tmp_path / "x.json") + backend.upsert( + [ + _row("a", [1.0, 0.0]), + _row("b", [0.0, 1.0]), + _row("c", [0.7, 0.7]), + ] + ) + hits = backend.query([1.0, 0.0], k=3) + assert hits[0][0].id == "a" + # scores are descending + scores = [s for _, s in hits] + assert scores == sorted(scores, reverse=True) + + +def test_json_backend_query_respects_k_limit(tmp_path: Path) -> None: + backend = _JsonBackend(tmp_path / "x.json") + backend.upsert([_row("a"), _row("b"), _row("c")]) + hits = backend.query([1.0, 0.0], k=2) + assert len(hits) == 2 + + +def test_json_backend_size_bytes_zero_when_missing(tmp_path: Path) -> None: + backend = _JsonBackend(tmp_path / "missing.json") + # File not yet flushed because no upsert + assert backend.size_bytes() == 0 + + +def test_json_backend_size_bytes_after_write(tmp_path: Path) -> None: + backend = _JsonBackend(tmp_path / "x.json") + backend.upsert([_row("a")]) + assert backend.size_bytes() > 0 + + +def test_json_backend_persists_across_instances(tmp_path: Path) -> None: + path = tmp_path / "x.json" + backend1 = _JsonBackend(path) + backend1.upsert([_row("a")]) + backend2 = _JsonBackend(path) + assert {r.id for r in backend2.all()} == {"a"} + + +def test_json_backend_recovers_from_corrupt_file(tmp_path: Path) -> None: + path = tmp_path / "x.json" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("not-json{{", encoding="utf-8") + backend = _JsonBackend(path) + assert backend.all() == [] + + +# --------------------------------------------------------------------------- +# Embedder.embed tests with fake encoder +# --------------------------------------------------------------------------- + + +class _FakeEncoder: + def __init__(self, dim: int = 8) -> None: + self.dim = dim + self.calls = 0 + + def encode(self, sentences: list[str], **kwargs: Any) -> list[list[float]]: + self.calls += 1 + out: list[list[float]] = [] + for s in sentences: + digest = hashlib.sha256(s.encode("utf-8")).digest() + vec = [float(digest[i % len(digest)]) for i in range(self.dim)] + norm = math.sqrt(sum(v * v for v in vec)) or 1.0 + vec = [v / norm for v in vec] + out.append(vec) + return out + + +def test_embedder_embed_empty_list_returns_empty() -> None: + enc = _FakeEncoder() + emb = Embedder(model="fake", dim=8, encoder=enc) + assert emb.embed([]) == [] + assert enc.calls == 0 + + +def test_embedder_embed_one_text() -> None: + enc = _FakeEncoder(dim=8) + emb = Embedder(model="fake", dim=8, encoder=enc) + out = emb.embed(["hello"]) + assert len(out) == 1 + assert len(out[0]) == 8 + + +def test_embedder_embed_multiple_texts() -> None: + enc = _FakeEncoder(dim=8) + emb = Embedder(model="fake", dim=8, encoder=enc) + out = emb.embed(["a", "b", "c"]) + assert len(out) == 3 + assert all(len(v) == 8 for v in out) + + +def test_embedder_embed_returns_floats() -> None: + enc = _FakeEncoder(dim=4) + emb = Embedder(model="fake", dim=4, encoder=enc) + out = emb.embed(["x"]) + assert all(isinstance(v, float) for v in out[0]) + + +def test_embedder_dim_property_uses_provided_value() -> None: + enc = _FakeEncoder(dim=8) + emb = Embedder(model="fake", dim=16, encoder=enc) + # When dim is explicitly set, it shouldn't trigger encoding. + assert emb.dim == 16 + assert enc.calls == 0 From 9207dbdefdff91ba7308e0609bb40a712cf281ff Mon Sep 17 00:00:00 2001 From: mochan Date: Mon, 27 Apr 2026 18:18:19 +0530 Subject: [PATCH 7/7] test: cover graph3d_transform.linkKey + app.formatQn (node tests) --- tests/test_app_format.js | 73 +++++++++++++++++++++++++++++++++ tests/test_graph3d_transform.js | 50 ++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 tests/test_app_format.js diff --git a/tests/test_app_format.js b/tests/test_app_format.js new file mode 100644 index 0000000..508bd28 --- /dev/null +++ b/tests/test_app_format.js @@ -0,0 +1,73 @@ +/* Tests for formatQn() in codegraph/web/static/app.js + * + * app.js is a classic browser script. Extract the `formatQn` function source + * (along with its `esc` dependency) and evaluate them in a Node vm sandbox. + * + * Run with: node --test tests/test_app_format.js + */ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); + +const APP_JS = path.join( + __dirname, '..', 'codegraph', 'web', 'static', 'app.js' +); + +function loadFormatQn() { + const source = fs.readFileSync(APP_JS, 'utf-8'); + const escMatch = source.match(/function esc\(s\)\s*\{[\s\S]*?\n\}/); + if (!escMatch) throw new Error('esc() not found in app.js'); + const fmtMatch = source.match(/function formatQn\(qn,\s*opts\)\s*\{[\s\S]*?\n\}/); + if (!fmtMatch) throw new Error('formatQn() not found in app.js'); + const sandbox = {}; + vm.createContext(sandbox); + vm.runInContext( + `${escMatch[0]}\n${fmtMatch[0]}\nthis.formatQn = formatQn;`, + sandbox, + ); + return sandbox.formatQn; +} + +const formatQn = loadFormatQn(); + +test('formatQn renders single-segment as leaf', () => { + const out = formatQn('foo', { maxParts: 3 }); + assert.match(out, /qn-key/); + assert.match(out, />foo { + const out = formatQn('a.b.c', { maxParts: 3 }); + assert.match(out, /qn-dim/); + assert.match(out, /a\.b\./); + assert.match(out, />c { + const out = formatQn('a.b.c.d.e', { maxParts: 2 }); + assert.match(out, /…/); + assert.match(out, />e { + const out = formatQn('a.', { maxParts: 3 }); + assert.match(out, /<b>/); + assert.doesNotMatch(out, //); +}); + +test('formatQn handles null gracefully', () => { + const out = formatQn(null, { maxParts: 3 }); + // null is coalesced to '' before splitting; one empty leaf, no head. + assert.match(out, /qn-key/); + assert.doesNotMatch(out, /null/); +}); + +test('formatQn defaults maxParts when opts omitted', () => { + const out = formatQn('a.b.c.d.e'); + // default maxParts = 3 -> visible head = last 2 segments + assert.match(out, /…/); +}); diff --git a/tests/test_graph3d_transform.js b/tests/test_graph3d_transform.js index ce13dae..93932f2 100644 --- a/tests/test_graph3d_transform.js +++ b/tests/test_graph3d_transform.js @@ -175,3 +175,53 @@ test('edgeColor varies by edge kind', () => { assert.notEqual(a, b); assert.notEqual(b, c); }); + +// --------------------------------------------------------------------------- +// linkKey() — pure helper used for dedup. Not exported; load via vm. +// --------------------------------------------------------------------------- + +const fs = require('node:fs'); +const vm = require('node:vm'); + +function loadLinkKey() { + const file = path.join( + __dirname, '..', 'codegraph', 'web', 'static', 'views', 'graph3d_transform.js' + ); + const source = fs.readFileSync(file, 'utf-8'); + const match = source.match(/function linkKey\(source,\s*target\)\s*\{[\s\S]*?\n\}/); + if (!match) throw new Error('linkKey() not found in graph3d_transform.js'); + const sandbox = {}; + vm.createContext(sandbox); + vm.runInContext(`${match[0]}\nthis.linkKey = linkKey;`, sandbox); + return sandbox.linkKey; +} + +const linkKey = loadLinkKey(); + +test('linkKey accepts plain string ids', () => { + assert.equal(linkKey('a', 'b'), 'a->b'); +}); + +test('linkKey extracts .id from object source', () => { + assert.equal(linkKey({ id: 'x' }, 'y'), 'x->y'); +}); + +test('linkKey extracts .id from object target', () => { + assert.equal(linkKey('a', { id: 'b' }), 'a->b'); +}); + +test('linkKey works for both objects', () => { + assert.equal(linkKey({ id: 'foo' }, { id: 'bar' }), 'foo->bar'); +}); + +test('linkKey stringifies non-string ids', () => { + assert.equal(linkKey(1, 2), '1->2'); +}); + +test('linkKey is deterministic — same inputs same output', () => { + assert.equal(linkKey('a', 'b'), linkKey('a', 'b')); +}); + +test('linkKey distinguishes direction', () => { + assert.notEqual(linkKey('a', 'b'), linkKey('b', 'a')); +});