mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-08 22:22:52 +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>
145 lines
4.7 KiB
TypeScript
145 lines
4.7 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
decideSkipGraph,
|
|
parseSkipGraphParam,
|
|
shouldConfirmGraphLoad,
|
|
} from '../../src/lib/graph-load-decision';
|
|
|
|
const THRESHOLD = 25_000;
|
|
const EDGE_THRESHOLD = 50_000;
|
|
|
|
describe('decideSkipGraph', () => {
|
|
it('auto-detects: skips when node count exceeds the threshold', () => {
|
|
expect(decideSkipGraph({ explicit: undefined, nodeCount: 300_000, threshold: THRESHOLD })).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it('auto-detects: keeps the full graph for small projects', () => {
|
|
expect(decideSkipGraph({ explicit: undefined, nodeCount: 500, threshold: THRESHOLD })).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it('explicit choice overrides auto-detection in both directions', () => {
|
|
// Force chat-only even for a tiny repo.
|
|
expect(decideSkipGraph({ explicit: true, nodeCount: 10, threshold: THRESHOLD })).toBe(true);
|
|
// Force a full graph even for a huge repo.
|
|
expect(decideSkipGraph({ explicit: false, nodeCount: 999_999, threshold: THRESHOLD })).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it('uses strictly-greater comparison at the threshold boundary', () => {
|
|
expect(
|
|
decideSkipGraph({ explicit: undefined, nodeCount: THRESHOLD, threshold: THRESHOLD }),
|
|
).toBe(false);
|
|
expect(
|
|
decideSkipGraph({ explicit: undefined, nodeCount: THRESHOLD + 1, threshold: THRESHOLD }),
|
|
).toBe(true);
|
|
});
|
|
|
|
it('fails open to a full download when the node count is unknown', () => {
|
|
expect(
|
|
decideSkipGraph({ explicit: undefined, nodeCount: undefined, threshold: THRESHOLD }),
|
|
).toBe(false);
|
|
expect(decideSkipGraph({ explicit: undefined, nodeCount: null, threshold: THRESHOLD })).toBe(
|
|
false,
|
|
);
|
|
expect(decideSkipGraph({ explicit: undefined, nodeCount: NaN, threshold: THRESHOLD })).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it('skips on the edge count even when nodes are under the node threshold', () => {
|
|
// Edge-heavy, node-light repo: 20K nodes (< 25K) but 80K edges (> 50K).
|
|
expect(
|
|
decideSkipGraph({
|
|
explicit: undefined,
|
|
nodeCount: 20_000,
|
|
threshold: THRESHOLD,
|
|
edgeCount: 80_000,
|
|
edgeThreshold: EDGE_THRESHOLD,
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it('does not skip when both node and edge counts are under their thresholds', () => {
|
|
expect(
|
|
decideSkipGraph({
|
|
explicit: undefined,
|
|
nodeCount: 5_000,
|
|
threshold: THRESHOLD,
|
|
edgeCount: 10_000,
|
|
edgeThreshold: EDGE_THRESHOLD,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('explicit choice overrides the edge auto-detect too', () => {
|
|
expect(
|
|
decideSkipGraph({
|
|
explicit: false,
|
|
nodeCount: 1,
|
|
threshold: THRESHOLD,
|
|
edgeCount: 999_999,
|
|
edgeThreshold: EDGE_THRESHOLD,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('fails open when edge count is unknown and nodes are under threshold', () => {
|
|
expect(
|
|
decideSkipGraph({
|
|
explicit: undefined,
|
|
nodeCount: 5_000,
|
|
threshold: THRESHOLD,
|
|
edgeCount: undefined,
|
|
edgeThreshold: EDGE_THRESHOLD,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('parseSkipGraphParam', () => {
|
|
it('parses affirmative values to true', () => {
|
|
expect(parseSkipGraphParam('1')).toBe(true);
|
|
expect(parseSkipGraphParam('true')).toBe(true);
|
|
expect(parseSkipGraphParam('TRUE')).toBe(true);
|
|
expect(parseSkipGraphParam(' true ')).toBe(true);
|
|
});
|
|
|
|
it('parses negative values to false', () => {
|
|
expect(parseSkipGraphParam('0')).toBe(false);
|
|
expect(parseSkipGraphParam('false')).toBe(false);
|
|
expect(parseSkipGraphParam('False')).toBe(false);
|
|
});
|
|
|
|
it('returns undefined for missing or unrecognized values', () => {
|
|
expect(parseSkipGraphParam(null)).toBeUndefined();
|
|
expect(parseSkipGraphParam(undefined)).toBeUndefined();
|
|
expect(parseSkipGraphParam('')).toBeUndefined();
|
|
expect(parseSkipGraphParam('yes')).toBeUndefined();
|
|
expect(parseSkipGraphParam('2')).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('shouldConfirmGraphLoad', () => {
|
|
it('confirms for a large repo', () => {
|
|
expect(shouldConfirmGraphLoad(300_000, THRESHOLD)).toBe(true);
|
|
expect(shouldConfirmGraphLoad(THRESHOLD + 1, THRESHOLD)).toBe(true);
|
|
});
|
|
|
|
it('does NOT confirm for a small repo at or below the threshold', () => {
|
|
expect(shouldConfirmGraphLoad(500, THRESHOLD)).toBe(false);
|
|
expect(shouldConfirmGraphLoad(THRESHOLD, THRESHOLD)).toBe(false);
|
|
});
|
|
|
|
it('confirms (fail-safe) when the node count is unknown', () => {
|
|
// The key regression guard: an unknown count must NOT silently re-load,
|
|
// which would risk re-introducing the #2178 hang.
|
|
expect(shouldConfirmGraphLoad(null, THRESHOLD)).toBe(true);
|
|
expect(shouldConfirmGraphLoad(undefined, THRESHOLD)).toBe(true);
|
|
expect(shouldConfirmGraphLoad(NaN, THRESHOLD)).toBe(true);
|
|
});
|
|
});
|