Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions tests/test_app_format.js
Original file line number Diff line number Diff line change
@@ -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</);
});

test('formatQn dims parent path', () => {
const out = formatQn('a.b.c', { maxParts: 3 });
assert.match(out, /qn-dim/);
assert.match(out, /a\.b\./);
assert.match(out, />c</);
});

test('formatQn truncates long paths with ellipsis', () => {
const out = formatQn('a.b.c.d.e', { maxParts: 2 });
assert.match(out, /…/);
assert.match(out, />e</);
});

test('formatQn escapes HTML in qn segments', () => {
const out = formatQn('a.<b>', { maxParts: 3 });
assert.match(out, /&lt;b&gt;/);
assert.doesNotMatch(out, /<b>/);
});

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, /…/);
});
92 changes: 92 additions & 0 deletions tests/test_diagrams_helpers.py
Original file line number Diff line number Diff line change
@@ -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
166 changes: 166 additions & 0 deletions tests/test_embed_helpers.py
Original file line number Diff line number Diff line change
@@ -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
50 changes: 50 additions & 0 deletions tests/test_graph3d_transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
});
Loading
Loading