mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +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.
272 lines
9.4 KiB
TypeScript
272 lines
9.4 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { createKnowledgeGraph } from '../../src/core/graph/graph';
|
|
|
|
// ==========================================================================
|
|
// PR1 Bug Fix Tests — positive and negative cases
|
|
// Tests the data structures and logic underlying the 4 bug fixes without
|
|
// requiring WASM (LadybugDB is skipped in test env via isTestEnv()).
|
|
// ==========================================================================
|
|
|
|
describe('createKnowledgeGraph — data integrity for loadServerGraph', () => {
|
|
// Positive: nodes and relationships are stored correctly
|
|
it('stores nodes added via addNode', () => {
|
|
const graph = createKnowledgeGraph();
|
|
graph.addNode({
|
|
id: 'Function:src/index.ts:main',
|
|
label: 'Function',
|
|
properties: { name: 'main', filePath: 'src/index.ts', startLine: 1, endLine: 10 },
|
|
});
|
|
|
|
expect(graph.nodes).toHaveLength(1);
|
|
expect(graph.nodes[0].id).toBe('Function:src/index.ts:main');
|
|
expect(graph.nodes[0].label).toBe('Function');
|
|
expect(graph.nodes[0].properties.name).toBe('main');
|
|
});
|
|
|
|
it('stores relationships added via addRelationship', () => {
|
|
const graph = createKnowledgeGraph();
|
|
graph.addNode({
|
|
id: 'Function:a.ts:foo',
|
|
label: 'Function',
|
|
properties: { name: 'foo', filePath: 'a.ts', startLine: 1, endLine: 5 },
|
|
});
|
|
graph.addNode({
|
|
id: 'Function:a.ts:bar',
|
|
label: 'Function',
|
|
properties: { name: 'bar', filePath: 'a.ts', startLine: 10, endLine: 15 },
|
|
});
|
|
graph.addRelationship({
|
|
sourceId: 'Function:a.ts:foo',
|
|
targetId: 'Function:a.ts:bar',
|
|
type: 'CALLS',
|
|
properties: {},
|
|
});
|
|
|
|
expect(graph.relationships).toHaveLength(1);
|
|
expect(graph.relationships[0].type).toBe('CALLS');
|
|
expect(graph.relationships[0].sourceId).toBe('Function:a.ts:foo');
|
|
expect(graph.relationships[0].targetId).toBe('Function:a.ts:bar');
|
|
});
|
|
|
|
// Positive: deduplication works
|
|
it('deduplicates nodes with the same id', () => {
|
|
const graph = createKnowledgeGraph();
|
|
graph.addNode({
|
|
id: 'File:src/index.ts',
|
|
label: 'File',
|
|
properties: { name: 'index.ts', filePath: 'src/index.ts' },
|
|
});
|
|
graph.addNode({
|
|
id: 'File:src/index.ts',
|
|
label: 'File',
|
|
properties: { name: 'index.ts', filePath: 'src/index.ts' },
|
|
});
|
|
|
|
expect(graph.nodes).toHaveLength(1);
|
|
});
|
|
|
|
// Positive: nodeCount reflects actual count
|
|
it('nodeCount matches number of unique nodes', () => {
|
|
const graph = createKnowledgeGraph();
|
|
graph.addNode({
|
|
id: 'File:a.ts',
|
|
label: 'File',
|
|
properties: { name: 'a.ts', filePath: 'a.ts' },
|
|
});
|
|
graph.addNode({
|
|
id: 'File:b.ts',
|
|
label: 'File',
|
|
properties: { name: 'b.ts', filePath: 'b.ts' },
|
|
});
|
|
|
|
expect(graph.nodeCount).toBe(2);
|
|
});
|
|
|
|
// Negative: empty graph has zero counts
|
|
it('empty graph has zero nodes and relationships', () => {
|
|
const graph = createKnowledgeGraph();
|
|
expect(graph.nodes).toHaveLength(0);
|
|
expect(graph.relationships).toHaveLength(0);
|
|
expect(graph.nodeCount).toBe(0);
|
|
});
|
|
|
|
// Negative: relationships with missing source/target still stored
|
|
// (validation is upstream, graph is a dumb container)
|
|
it('stores relationships even with non-existent node IDs', () => {
|
|
const graph = createKnowledgeGraph();
|
|
graph.addRelationship({
|
|
sourceId: 'NonExistent:a',
|
|
targetId: 'NonExistent:b',
|
|
type: 'CALLS',
|
|
properties: {},
|
|
});
|
|
|
|
expect(graph.relationships).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('loadServerGraph — data flow validation', () => {
|
|
// Positive: server data can be reconstructed into a KnowledgeGraph
|
|
it('reconstructs graph from server node/relationship arrays', () => {
|
|
const graph = createKnowledgeGraph();
|
|
const serverNodes = [
|
|
{
|
|
id: 'File:src/app.ts',
|
|
label: 'File' as const,
|
|
properties: { name: 'app.ts', filePath: 'src/app.ts' },
|
|
},
|
|
{
|
|
id: 'Function:src/app.ts:main',
|
|
label: 'Function' as const,
|
|
properties: { name: 'main', filePath: 'src/app.ts', startLine: 1, endLine: 20 },
|
|
},
|
|
];
|
|
const serverRels = [
|
|
{
|
|
sourceId: 'File:src/app.ts',
|
|
targetId: 'Function:src/app.ts:main',
|
|
type: 'CONTAINS' as const,
|
|
properties: {},
|
|
},
|
|
];
|
|
|
|
for (const node of serverNodes) graph.addNode(node);
|
|
for (const rel of serverRels) graph.addRelationship(rel);
|
|
|
|
expect(graph.nodeCount).toBe(2);
|
|
expect(graph.relationships).toHaveLength(1);
|
|
expect(graph.relationships[0].type).toBe('CONTAINS');
|
|
});
|
|
|
|
// Positive: file contents map is built correctly from server data
|
|
it('builds fileContents Map from server object entries', () => {
|
|
const serverFileContents: Record<string, string> = {
|
|
'src/index.ts': 'export function main() {}',
|
|
'src/utils.ts': 'export const helper = () => {}',
|
|
};
|
|
|
|
const fileMap = new Map<string, string>();
|
|
for (const [path, content] of Object.entries(serverFileContents)) {
|
|
fileMap.set(path, content);
|
|
}
|
|
|
|
expect(fileMap.size).toBe(2);
|
|
expect(fileMap.get('src/index.ts')).toBe('export function main() {}');
|
|
expect(fileMap.get('src/utils.ts')).toContain('helper');
|
|
});
|
|
|
|
// Negative: empty server response produces empty graph
|
|
it('handles empty server data gracefully', () => {
|
|
const graph = createKnowledgeGraph();
|
|
const serverNodes: any[] = [];
|
|
const serverRels: any[] = [];
|
|
|
|
for (const node of serverNodes) graph.addNode(node);
|
|
for (const rel of serverRels) graph.addRelationship(rel);
|
|
|
|
expect(graph.nodeCount).toBe(0);
|
|
expect(graph.relationships).toHaveLength(0);
|
|
});
|
|
|
|
// Negative: fileContents replaces (not accumulates) on reload
|
|
it('fileContents map replacement prevents stale data', () => {
|
|
// Simulates the storedFileContents = fileMap assignment in loadServerGraph
|
|
let storedFileContents = new Map<string, string>();
|
|
storedFileContents.set('old-file.ts', 'old content');
|
|
|
|
// Second load replaces the map entirely
|
|
const newFileMap = new Map<string, string>();
|
|
newFileMap.set('new-file.ts', 'new content');
|
|
storedFileContents = newFileMap; // assignment, not merge
|
|
|
|
expect(storedFileContents.size).toBe(1);
|
|
expect(storedFileContents.has('old-file.ts')).toBe(false);
|
|
expect(storedFileContents.get('new-file.ts')).toBe('new content');
|
|
});
|
|
});
|
|
|
|
describe('BM25 index — argument type validation', () => {
|
|
// Positive: Map<string, string> has the expected interface for BM25
|
|
it('Map has entries() and size for BM25 indexing', () => {
|
|
const fileMap = new Map<string, string>([
|
|
['src/a.ts', 'function foo() {}'],
|
|
['src/b.ts', 'function bar() {}'],
|
|
]);
|
|
|
|
expect(fileMap.size).toBe(2);
|
|
expect(typeof fileMap.entries).toBe('function');
|
|
|
|
// Verify iteration works (BM25 iterates entries)
|
|
const entries = Array.from(fileMap.entries());
|
|
expect(entries).toHaveLength(2);
|
|
expect(entries[0][0]).toBe('src/a.ts');
|
|
});
|
|
|
|
// Negative: a KnowledgeGraph object does NOT have entries()
|
|
// This was the original bug — passing graph instead of fileMap
|
|
it('KnowledgeGraph does not have entries() (the original bug)', () => {
|
|
const graph = createKnowledgeGraph();
|
|
expect((graph as any).entries).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('highlight clearing — state management', () => {
|
|
// Positive: Set operations for highlight clearing
|
|
it('clearing a Set produces an empty set', () => {
|
|
const highlights = new Set(['node1', 'node2', 'node3']);
|
|
const cleared = new Set<string>();
|
|
|
|
expect(cleared.size).toBe(0);
|
|
expect(highlights.size).toBe(3);
|
|
});
|
|
|
|
// Positive: multiple highlight sources are independent
|
|
it('independent highlight sets can be cleared separately', () => {
|
|
const processHighlights = new Set(['proc_1', 'proc_2']);
|
|
const aiToolHighlights = new Set(['Function:a.ts:foo']);
|
|
const aiCitationHighlights = new Set(['File:b.ts']);
|
|
const blastRadius = new Set(['Function:c.ts:bar']);
|
|
|
|
// Simulate "Turn off all highlights" — clear all sets
|
|
const clearedProcess = new Set<string>();
|
|
const clearedAITool = new Set<string>();
|
|
const clearedAICitation = new Set<string>();
|
|
const clearedBlast = new Set<string>();
|
|
|
|
expect(clearedProcess.size).toBe(0);
|
|
expect(clearedAITool.size).toBe(0);
|
|
expect(clearedAICitation.size).toBe(0);
|
|
expect(clearedBlast.size).toBe(0);
|
|
|
|
// Original sets unchanged (React state immutability)
|
|
expect(processHighlights.size).toBe(2);
|
|
expect(aiToolHighlights.size).toBe(1);
|
|
});
|
|
|
|
// Negative: clearing highlights doesn't affect node selection
|
|
// (selection is a separate state — verified by checking they're independent)
|
|
it('highlight state is independent from node selection state', () => {
|
|
const highlights = new Set(['node1']);
|
|
let selectedNode: { id: string } | null = { id: 'node1' };
|
|
|
|
// Clear highlights but keep selection
|
|
const clearedHighlights = new Set<string>();
|
|
expect(clearedHighlights.size).toBe(0);
|
|
expect(selectedNode).not.toBeNull();
|
|
|
|
// Clear selection independently
|
|
selectedNode = null;
|
|
expect(selectedNode).toBeNull();
|
|
});
|
|
|
|
// Negative: toggling AI highlights ON should NOT clear user query highlights
|
|
it('AI highlight toggle does not clear process highlights', () => {
|
|
const processHighlights = new Set(['proc_1', 'proc_2']);
|
|
let isAIEnabled = false;
|
|
|
|
// Turn AI on — process highlights should survive
|
|
isAIEnabled = true;
|
|
expect(processHighlights.size).toBe(2);
|
|
expect(isAIEnabled).toBe(true);
|
|
});
|
|
});
|