mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-06 08:16:02 +00:00
* feat: configure prettier with pre-commit hook integration Add prettier, lint-staged, and prettier-plugin-tailwindcss at the repo root with husky pre-commit hook integration. Moves husky from gitnexus/ to root package.json for reliable hook installation. - Root package.json with prepare/format/format:check scripts - .prettierrc with endOfLine:lf and tailwindStylesheet for TW v4 - .prettierignore excluding fixtures, vendor, generated, *.d.ts, *.md - .gitattributes enforcing LF line endings for Windows consistency - Pre-commit hook uses direct node_modules/.bin/ paths (no npx) * style: apply prettier formatting to entire codebase One-time bulk format. No logic changes. Use .git-blame-ignore-revs to skip this commit in git blame. * chore: add .git-blame-ignore-revs for prettier format commit * perf: pre-commit hook runs only tests related to staged files Use vitest --related to scope test execution to tests that import the changed files, instead of running the full suite on every commit. * perf: remove vitest from pre-commit hook, keep in CI only Pre-commit now runs lint-staged + tsc only. Tests run in CI (ci-tests.yml) where they belong — keeps commits fast. * ci: add prettier format check to quality workflow PRs will now fail if code isn't formatted with prettier.
250 lines
8.7 KiB
TypeScript
250 lines
8.7 KiB
TypeScript
/**
|
|
* P0 Unit Tests: Knowledge Graph
|
|
*
|
|
* Tests: createKnowledgeGraph() — addNode, getNode, removeNode,
|
|
* iterNodes, addRelationship, removeNodesByFile, counts.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import { createKnowledgeGraph } from '../../src/core/graph/graph.js';
|
|
import type { GraphNode, GraphRelationship } from '../../src/core/graph/types.js';
|
|
|
|
function makeNode(id: string, name: string, filePath: string = 'src/test.ts'): GraphNode {
|
|
return {
|
|
id,
|
|
label: 'Function',
|
|
properties: { name, filePath, startLine: 1, endLine: 10 },
|
|
};
|
|
}
|
|
|
|
function makeRel(
|
|
src: string,
|
|
tgt: string,
|
|
type: GraphRelationship['type'] = 'CALLS',
|
|
): GraphRelationship {
|
|
return {
|
|
id: `${src}-${type}-${tgt}`,
|
|
sourceId: src,
|
|
targetId: tgt,
|
|
type,
|
|
confidence: 1.0,
|
|
reason: '',
|
|
};
|
|
}
|
|
|
|
describe('createKnowledgeGraph', () => {
|
|
// ─── addNode / getNode ─────────────────────────────────────────────
|
|
|
|
it('adds and retrieves a node', () => {
|
|
const g = createKnowledgeGraph();
|
|
const node = makeNode('fn:foo', 'foo');
|
|
g.addNode(node);
|
|
expect(g.getNode('fn:foo')).toBe(node);
|
|
});
|
|
|
|
it('returns undefined for unknown node', () => {
|
|
const g = createKnowledgeGraph();
|
|
expect(g.getNode('nonexistent')).toBeUndefined();
|
|
});
|
|
|
|
it('duplicate addNode is a no-op', () => {
|
|
const g = createKnowledgeGraph();
|
|
const node1 = makeNode('fn:foo', 'foo');
|
|
const node2 = makeNode('fn:foo', 'bar'); // same ID, different name
|
|
g.addNode(node1);
|
|
g.addNode(node2);
|
|
expect(g.nodeCount).toBe(1);
|
|
expect(g.getNode('fn:foo')!.properties.name).toBe('foo'); // first one wins
|
|
});
|
|
|
|
// ─── removeNode ─────────────────────────────────────────────────────
|
|
|
|
it('removes a node and its relationships', () => {
|
|
const g = createKnowledgeGraph();
|
|
g.addNode(makeNode('fn:a', 'a'));
|
|
g.addNode(makeNode('fn:b', 'b'));
|
|
g.addRelationship(makeRel('fn:a', 'fn:b'));
|
|
expect(g.relationshipCount).toBe(1);
|
|
|
|
const removed = g.removeNode('fn:a');
|
|
expect(removed).toBe(true);
|
|
expect(g.getNode('fn:a')).toBeUndefined();
|
|
expect(g.nodeCount).toBe(1);
|
|
expect(g.relationshipCount).toBe(0); // relationship involving fn:a removed
|
|
});
|
|
|
|
it('removeNode returns false for unknown node', () => {
|
|
const g = createKnowledgeGraph();
|
|
expect(g.removeNode('nope')).toBe(false);
|
|
});
|
|
|
|
// ─── removeNodesByFile ──────────────────────────────────────────────
|
|
|
|
it('removes all nodes belonging to a file', () => {
|
|
const g = createKnowledgeGraph();
|
|
g.addNode(makeNode('fn:a', 'a', 'src/foo.ts'));
|
|
g.addNode(makeNode('fn:b', 'b', 'src/foo.ts'));
|
|
g.addNode(makeNode('fn:c', 'c', 'src/bar.ts'));
|
|
|
|
const removed = g.removeNodesByFile('src/foo.ts');
|
|
expect(removed).toBe(2);
|
|
expect(g.nodeCount).toBe(1);
|
|
expect(g.getNode('fn:c')).toBeDefined();
|
|
});
|
|
|
|
// ─── iterNodes / iterRelationships ─────────────────────────────────
|
|
|
|
it('iterNodes yields all nodes', () => {
|
|
const g = createKnowledgeGraph();
|
|
g.addNode(makeNode('fn:a', 'a'));
|
|
g.addNode(makeNode('fn:b', 'b'));
|
|
|
|
const ids = [...g.iterNodes()].map((n) => n.id);
|
|
expect(ids).toHaveLength(2);
|
|
expect(ids).toContain('fn:a');
|
|
expect(ids).toContain('fn:b');
|
|
});
|
|
|
|
it('iterRelationships yields all relationships', () => {
|
|
const g = createKnowledgeGraph();
|
|
g.addNode(makeNode('fn:a', 'a'));
|
|
g.addNode(makeNode('fn:b', 'b'));
|
|
g.addRelationship(makeRel('fn:a', 'fn:b'));
|
|
|
|
const rels = [...g.iterRelationships()];
|
|
expect(rels).toHaveLength(1);
|
|
expect(rels[0].sourceId).toBe('fn:a');
|
|
});
|
|
|
|
// ─── nodeCount / relationshipCount ─────────────────────────────────
|
|
|
|
it('nodeCount reflects current node count', () => {
|
|
const g = createKnowledgeGraph();
|
|
expect(g.nodeCount).toBe(0);
|
|
g.addNode(makeNode('fn:a', 'a'));
|
|
expect(g.nodeCount).toBe(1);
|
|
g.addNode(makeNode('fn:b', 'b'));
|
|
expect(g.nodeCount).toBe(2);
|
|
});
|
|
|
|
it('relationshipCount reflects current relationship count', () => {
|
|
const g = createKnowledgeGraph();
|
|
g.addNode(makeNode('fn:a', 'a'));
|
|
g.addNode(makeNode('fn:b', 'b'));
|
|
expect(g.relationshipCount).toBe(0);
|
|
g.addRelationship(makeRel('fn:a', 'fn:b'));
|
|
expect(g.relationshipCount).toBe(1);
|
|
});
|
|
|
|
// ─── addRelationship ───────────────────────────────────────────────
|
|
|
|
it('duplicate addRelationship is a no-op', () => {
|
|
const g = createKnowledgeGraph();
|
|
g.addNode(makeNode('fn:a', 'a'));
|
|
g.addNode(makeNode('fn:b', 'b'));
|
|
g.addRelationship(makeRel('fn:a', 'fn:b'));
|
|
g.addRelationship(makeRel('fn:a', 'fn:b')); // same ID
|
|
expect(g.relationshipCount).toBe(1);
|
|
});
|
|
|
|
// ─── nodes / relationships arrays ──────────────────────────────────
|
|
|
|
it('.nodes returns an array copy', () => {
|
|
const g = createKnowledgeGraph();
|
|
g.addNode(makeNode('fn:a', 'a'));
|
|
const arr1 = g.nodes;
|
|
const arr2 = g.nodes;
|
|
expect(arr1).not.toBe(arr2); // different array instances
|
|
expect(arr1).toHaveLength(1);
|
|
});
|
|
|
|
it('.relationships returns an array copy', () => {
|
|
const g = createKnowledgeGraph();
|
|
g.addNode(makeNode('fn:a', 'a'));
|
|
g.addNode(makeNode('fn:b', 'b'));
|
|
g.addRelationship(makeRel('fn:a', 'fn:b'));
|
|
const arr1 = g.relationships;
|
|
const arr2 = g.relationships;
|
|
expect(arr1).not.toBe(arr2);
|
|
expect(arr1).toHaveLength(1);
|
|
});
|
|
|
|
// ─── forEachNode / forEachRelationship ──────────────────────────────
|
|
|
|
it('forEachNode calls fn for every node', () => {
|
|
const g = createKnowledgeGraph();
|
|
g.addNode(makeNode('fn:a', 'a'));
|
|
g.addNode(makeNode('fn:b', 'b'));
|
|
|
|
const ids: string[] = [];
|
|
g.forEachNode((n) => ids.push(n.id));
|
|
expect(ids).toHaveLength(2);
|
|
});
|
|
|
|
it('forEachRelationship calls fn for every relationship', () => {
|
|
const g = createKnowledgeGraph();
|
|
g.addNode(makeNode('fn:a', 'a'));
|
|
g.addNode(makeNode('fn:b', 'b'));
|
|
g.addRelationship(makeRel('fn:a', 'fn:b'));
|
|
|
|
const types: string[] = [];
|
|
g.forEachRelationship((r) => types.push(r.type));
|
|
expect(types).toEqual(['CALLS']);
|
|
});
|
|
|
|
// ─── removeRelationship ─────────────────────────────────────────────
|
|
|
|
it('removes a relationship by id', () => {
|
|
const g = createKnowledgeGraph();
|
|
g.addNode(makeNode('fn:a', 'a'));
|
|
g.addNode(makeNode('fn:b', 'b'));
|
|
g.addRelationship(makeRel('fn:a', 'fn:b'));
|
|
expect(g.relationshipCount).toBe(1);
|
|
|
|
const removed = g.removeRelationship('fn:a-CALLS-fn:b');
|
|
expect(removed).toBe(true);
|
|
expect(g.relationshipCount).toBe(0);
|
|
});
|
|
|
|
it('removeRelationship returns false for unknown id', () => {
|
|
const g = createKnowledgeGraph();
|
|
expect(g.removeRelationship('nonexistent')).toBe(false);
|
|
});
|
|
|
|
it('removeRelationship returns false on second call with same id', () => {
|
|
const g = createKnowledgeGraph();
|
|
g.addNode(makeNode('fn:a', 'a'));
|
|
g.addNode(makeNode('fn:b', 'b'));
|
|
g.addRelationship(makeRel('fn:a', 'fn:b'));
|
|
|
|
expect(g.removeRelationship('fn:a-CALLS-fn:b')).toBe(true);
|
|
expect(g.removeRelationship('fn:a-CALLS-fn:b')).toBe(false);
|
|
});
|
|
|
|
it('removeRelationship does not affect nodes', () => {
|
|
const g = createKnowledgeGraph();
|
|
g.addNode(makeNode('fn:a', 'a'));
|
|
g.addNode(makeNode('fn:b', 'b'));
|
|
g.addRelationship(makeRel('fn:a', 'fn:b'));
|
|
|
|
g.removeRelationship('fn:a-CALLS-fn:b');
|
|
expect(g.nodeCount).toBe(2);
|
|
expect(g.getNode('fn:a')).toBeDefined();
|
|
expect(g.getNode('fn:b')).toBeDefined();
|
|
});
|
|
|
|
it('removeRelationship leaves other relationships intact', () => {
|
|
const g = createKnowledgeGraph();
|
|
g.addNode(makeNode('fn:a', 'a'));
|
|
g.addNode(makeNode('fn:b', 'b'));
|
|
g.addNode(makeNode('fn:c', 'c'));
|
|
g.addRelationship(makeRel('fn:a', 'fn:b'));
|
|
g.addRelationship(makeRel('fn:b', 'fn:c'));
|
|
expect(g.relationshipCount).toBe(2);
|
|
|
|
g.removeRelationship('fn:a-CALLS-fn:b');
|
|
expect(g.relationshipCount).toBe(1);
|
|
const remaining = [...g.iterRelationships()];
|
|
expect(remaining[0].sourceId).toBe('fn:b');
|
|
expect(remaining[0].targetId).toBe('fn:c');
|
|
});
|
|
});
|