mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* feat(web): add graph-load skip decision helper and node threshold (#2178) * feat(web): skip graph download in connectToServer for chat-only mode (#2178) * feat(web): add graphMode state and empty-graph chat-only handling (#2178) * feat(web): read and thread ?skipGraph URL param through connect flow (#2178) * feat(web): chat-only empty state with load-graph-anyway escape hatch (#2178) * style(web): apply prettier formatting to graph-load files (#2178) * fix(review): apply autofix feedback - Fail-safe confirm + authoritative node count (P1: prevent re-triggering the hang via Load-graph-anyway when count unknown) - In-flight guard on loadGraphAnyway (P1: double-fire) - Honor explicit ?skipGraph in onAnalyzeComplete and DropZone (R6/U4) - Extract buildGraphFromConnectResult shared helper (DRY across 3 connect sites) - Add tests: switchRepo skip path, threshold config override, loadGraphAnyway error path, confirm fail-safe, in-flight guard * fix(review): address tri-review findings - P1 (correctness+adversarial+risk): stop the cross-repo / F5 chat-only leak. loadGraphAnyway no longer persists ?skipGraph=0, and onAnalyzeComplete + DropZone no longer inherit a stale ?skipGraph for a different repo — both could bypass auto-detect and re-trigger the #2178 hang. ?skipGraph is now a bookmark hint honored only by the initial auto-connect; in-session repo changes auto-detect. - P2 (performance): auto-detect now also skips on edge count (edge-driven force-layout cliff), not just nodes; LARGE_GRAPH_EDGE_THRESHOLD default 50K. - P2 (julik): reset graphMode/chatOnlyNodeCount at the top of switchRepo so a failed switch can't leave a stale chat-only overlay. - P2 (julik): set serverBaseUrl before awaiting handleServerConnect in auto-connect so the Load-graph-anyway button isn't briefly a no-op. - P2 (risk): hide the misleading '0 nodes / 0 edges' stats in chat-only mode (Header + StatusBar). - P2 (performance): guard the GraphCanvas layout effect against the empty chat-only graph. - Tests: edge-threshold decision + connectToServer edge-trigger; load-anyway no longer asserts URL persistence. * fix(web): make Load-graph-anyway cancellable, unmount-safe, fail-safe confirm (#2178) - AbortController + mountedRef: cancel the in-flight download on unmount and guard every post-await setState by the mounted ref (an abort surfaces as a BackendError, not a DOMException AbortError, so name-checks would miss it) - Stale-result guard: a load-anyway that resolves after a concurrent switchRepo no longer clobbers the new repo's graph/mode/count - GraphCanvas confirm fails SAFE (treat as declined) when window.confirm is unavailable or throws, instead of silently proceeding into a large download * fix(web): make the AI agent and chat surface aware of chat-only mode (#2178) - buildDynamicSystemPrompt + createGraphRAGAgent take a chatOnly flag and append a note (both prompt branches) that supersedes VISUAL GROUNDING: the graph isn't loaded, [[Type:Name]] node citations won't highlight, prefer [[path:START-END]] - initializeAgent resolves chatOnly = opts ?? graphModeRef.current==='chatOnly': connect-flow callers (handleServerConnect, switchRepo, loadGraphAnyway re-init) pass it explicitly; lazy/settings re-inits fall back to live mode via the ref - loadGraphAnyway re-inits the agent (chatOnly:false) after a full load so the prompt drops the note - RightPanel shows a chat-only banner so the degradation is visible where AI output renders (en + zh-CN) * fix(web): streaming circuit breaker for graphs with missing size stats (#2178) - GraphTooLargeError + a mid-stream breaker in parseNdjsonGraphResponse: count nodes/relationships as they arrive and abort (cancel reader in try/finally, then throw) the moment either crosses its limit — reusing the existing node/ edge thresholds, no new magic constant. Throwing right after the offending push means a later error record in the same chunk can't pre-empt it. - fetchGraph gains optional maxNodes/maxEdges (off by default → existing callers unchanged). connectToServer arms them only for auto-detect downloads (skipGraph !== false) and catches GraphTooLargeError → chat-only, re-throwing every other error. This backstops the no-stats fail-open path that could otherwise re-trigger the original hang. * chore(autofix): apply prettier + eslint fixes via /autofix command --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
112 lines
4.2 KiB
TypeScript
112 lines
4.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { BASE_SYSTEM_PROMPT } from '../../src/core/llm/agent';
|
|
import { buildDynamicSystemPrompt, type CodebaseContext } from '../../src/core/llm/context-builder';
|
|
import {
|
|
createGraphRAGTools,
|
|
GRAPH_RAG_TOOL_NAMES,
|
|
type GraphRAGBackend,
|
|
} from '../../src/core/llm/tools';
|
|
import { NODE_REF_REGEX } from '../../src/lib/grounding-patterns';
|
|
|
|
const MINIMAL_CONTEXT: CodebaseContext = {
|
|
stats: {
|
|
projectName: 'proj',
|
|
fileCount: 0,
|
|
functionCount: 0,
|
|
classCount: 0,
|
|
interfaceCount: 0,
|
|
methodCount: 0,
|
|
},
|
|
hotspots: [],
|
|
folderTree: '',
|
|
};
|
|
|
|
/** Legacy or phantom tool names that must not appear in the system prompt. */
|
|
const FORBIDDEN_TOOL_NAMES = [
|
|
'hybrid_search',
|
|
'semantic_search',
|
|
'semantic_search_with_context',
|
|
'execute_cypher',
|
|
'execute_vector_cypher',
|
|
'grep_code',
|
|
'read_file',
|
|
'get_graph_schema',
|
|
'get_code_content',
|
|
'get_codebase_stats',
|
|
] as const;
|
|
|
|
/**
|
|
* No-op backend. createGraphRAGTools only captures these methods inside each tool's
|
|
* async execute closure — it never invokes them at construction time — so empty
|
|
* implementations are enough to build the tools and read their registered names.
|
|
*/
|
|
const stubBackend: GraphRAGBackend = {
|
|
executeQuery: async () => [],
|
|
search: async () => [],
|
|
grep: async () => [],
|
|
readFile: async () => '',
|
|
};
|
|
|
|
describe('BASE_SYSTEM_PROMPT tool parity', () => {
|
|
it('documents every registered Graph RAG tool by exact name', () => {
|
|
for (const name of GRAPH_RAG_TOOL_NAMES) {
|
|
expect(BASE_SYSTEM_PROMPT).toContain(`\`${name}\``);
|
|
}
|
|
});
|
|
|
|
it('keeps GRAPH_RAG_TOOL_NAMES in sync with the tools createGraphRAGTools registers', () => {
|
|
const registered = createGraphRAGTools(stubBackend).map((t) => t.name);
|
|
expect(registered.sort()).toEqual([...GRAPH_RAG_TOOL_NAMES].sort());
|
|
});
|
|
|
|
it('does not reference legacy or non-existent tool names', () => {
|
|
for (const name of FORBIDDEN_TOOL_NAMES) {
|
|
// Word-boundary match catches both backticked and bare-prose mentions.
|
|
expect(BASE_SYSTEM_PROMPT).not.toMatch(new RegExp(`\\b${name}\\b`));
|
|
}
|
|
});
|
|
|
|
it('uses explicit file citation format expected by the UI parser', () => {
|
|
expect(BASE_SYSTEM_PROMPT).toMatch(/\[\[src\/[^\]]+:\d+-\d+\]\]/);
|
|
expect(BASE_SYSTEM_PROMPT).not.toContain('[[file:line]]');
|
|
});
|
|
|
|
it('documents a parser-recognized symbol citation format', () => {
|
|
// Use the UI parser's own allowlist (NODE_REF_REGEX) so this tracks the parser
|
|
// instead of forking its label list. NODE_REF_REGEX is /g; use a non-global copy
|
|
// so the match is stateless.
|
|
expect(BASE_SYSTEM_PROMPT).toMatch(new RegExp(NODE_REF_REGEX.source));
|
|
});
|
|
|
|
it('documents typed node labels, not polymorphic CodeNode', () => {
|
|
expect(BASE_SYSTEM_PROMPT).toContain('MATCH (f:Function)');
|
|
expect(BASE_SYSTEM_PROMPT).not.toContain('CodeNode');
|
|
expect(BASE_SYSTEM_PROMPT).not.toContain('INHERITS');
|
|
});
|
|
|
|
it('clarifies highlight_in_graph is not a callable tool', () => {
|
|
// Reword-proof, registry-level guarantee: the load-bearing fact is that
|
|
// highlight_in_graph is not a registered tool, regardless of prompt phrasing.
|
|
expect(GRAPH_RAG_TOOL_NAMES).not.toContain('highlight_in_graph');
|
|
// The prompt still addresses it explicitly...
|
|
expect(BASE_SYSTEM_PROMPT).toContain('highlight_in_graph');
|
|
// ...and must never instruct the model to call it (guards an affirmative reword).
|
|
expect(BASE_SYSTEM_PROMPT).not.toMatch(/\b(?:use|call|invoke)\s+`?highlight_in_graph/i);
|
|
});
|
|
});
|
|
|
|
describe('buildDynamicSystemPrompt chat-only mode (#2178)', () => {
|
|
it('appends a chat-only note that overrides VISUAL GROUNDING when chatOnly', () => {
|
|
const prompt = buildDynamicSystemPrompt(BASE_SYSTEM_PROMPT, MINIMAL_CONTEXT, true);
|
|
expect(prompt).toContain('CHAT-ONLY MODE');
|
|
expect(prompt).toMatch(/node citations will NOT highlight/i);
|
|
expect(prompt).toContain('[[path:START-END]]');
|
|
});
|
|
|
|
it('leaves the prompt unchanged when chatOnly is false/omitted', () => {
|
|
const full = buildDynamicSystemPrompt(BASE_SYSTEM_PROMPT, MINIMAL_CONTEXT);
|
|
const explicitFalse = buildDynamicSystemPrompt(BASE_SYSTEM_PROMPT, MINIMAL_CONTEXT, false);
|
|
expect(full).toBe(explicitFalse);
|
|
expect(full).not.toContain('CHAT-ONLY MODE');
|
|
});
|
|
});
|