mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-07 08:26:11 +00:00
* fix(web): stream large graph responses * fix(server): harden graph streaming * fix(ci): stabilize graph loading coverage --------- Co-authored-by: gfwangjie <gfwangjie@gf.com.cn>
167 lines
5 KiB
TypeScript
167 lines
5 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { fetchGraph, normalizeServerUrl, setBackendUrl } from '../../src/services/backend-client';
|
|
|
|
describe('normalizeServerUrl', () => {
|
|
it('adds http:// to localhost', () => {
|
|
expect(normalizeServerUrl('localhost:4747')).toBe('http://localhost:4747');
|
|
});
|
|
|
|
it('adds http:// to 127.0.0.1', () => {
|
|
expect(normalizeServerUrl('127.0.0.1:4747')).toBe('http://127.0.0.1:4747');
|
|
});
|
|
|
|
it('adds https:// to non-local hosts', () => {
|
|
expect(normalizeServerUrl('example.com')).toBe('https://example.com');
|
|
});
|
|
|
|
it('strips trailing slashes', () => {
|
|
expect(normalizeServerUrl('http://localhost:4747/')).toBe('http://localhost:4747');
|
|
expect(normalizeServerUrl('http://localhost:4747///')).toBe('http://localhost:4747');
|
|
});
|
|
|
|
it('strips /api suffix (base URL only)', () => {
|
|
expect(normalizeServerUrl('http://localhost:4747/api')).toBe('http://localhost:4747');
|
|
});
|
|
|
|
it('trims whitespace', () => {
|
|
expect(normalizeServerUrl(' localhost:4747 ')).toBe('http://localhost:4747');
|
|
});
|
|
|
|
it('preserves existing https://', () => {
|
|
expect(normalizeServerUrl('https://gitnexus.example.com')).toBe('https://gitnexus.example.com');
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('fetchGraph', () => {
|
|
it('requests streamed graph responses from the backend', async () => {
|
|
setBackendUrl('http://localhost:4747');
|
|
|
|
const fetchMock = vi.fn().mockResolvedValue(
|
|
new Response('{"nodes":[],"relationships":[]}', {
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
}),
|
|
);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
await fetchGraph('big-repo');
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
expect.stringContaining('/api/graph?repo=big-repo&stream=true'),
|
|
expect.any(Object),
|
|
);
|
|
});
|
|
|
|
it('parses NDJSON graph streams incrementally', async () => {
|
|
setBackendUrl('http://localhost:4747');
|
|
|
|
const encoder = new TextEncoder();
|
|
const stream = new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(
|
|
encoder.encode(
|
|
[
|
|
'{"type":"node","data":{"id":"File:src/app.ts","label":"File","properties":{"name":"app.ts","filePath":"src/app.ts"}}}\n',
|
|
'{"type":"relationship","data":{"id":"File:src/app.ts_CONTAINS_Function:src/app.ts:main","type":"CONTAINS","sourceId":"File:src/app.ts","targetId":"Function:src/app.ts:main"}}\n',
|
|
].join(''),
|
|
),
|
|
);
|
|
controller.close();
|
|
},
|
|
});
|
|
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue(
|
|
new Response(stream, {
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': 'application/x-ndjson',
|
|
},
|
|
}),
|
|
),
|
|
);
|
|
|
|
const progress = vi.fn();
|
|
const result = await fetchGraph('big-repo', { onProgress: progress });
|
|
|
|
expect(result.nodes).toHaveLength(1);
|
|
expect(result.relationships).toHaveLength(1);
|
|
expect(result.nodes[0].id).toBe('File:src/app.ts');
|
|
expect(result.relationships[0].type).toBe('CONTAINS');
|
|
expect(progress).toHaveBeenCalled();
|
|
});
|
|
|
|
it('parses NDJSON graph lines split across chunks', async () => {
|
|
setBackendUrl('http://localhost:4747');
|
|
|
|
const encoder = new TextEncoder();
|
|
const stream = new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(
|
|
encoder.encode(
|
|
'{"type":"node","data":{"id":"File:src/app.ts","label":"File","properties":{"name":"app.ts"',
|
|
),
|
|
);
|
|
controller.enqueue(
|
|
encoder.encode(
|
|
',"filePath":"src/app.ts"}}}\n{"type":"relationship","data":{"id":"File:src/app.ts_CONTAINS_Function:src/app.ts:main","type":"CONTAINS","sourceId":"File:src/app.ts","targetId":"Function:src/app.ts:main"}}\n',
|
|
),
|
|
);
|
|
controller.close();
|
|
},
|
|
});
|
|
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue(
|
|
new Response(stream, {
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': 'application/x-ndjson',
|
|
},
|
|
}),
|
|
),
|
|
);
|
|
|
|
const result = await fetchGraph('big-repo');
|
|
|
|
expect(result.nodes).toHaveLength(1);
|
|
expect(result.relationships).toHaveLength(1);
|
|
expect(result.nodes[0].properties.filePath).toBe('src/app.ts');
|
|
});
|
|
|
|
it('throws backend errors emitted in the NDJSON stream', async () => {
|
|
setBackendUrl('http://localhost:4747');
|
|
|
|
const encoder = new TextEncoder();
|
|
const stream = new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(encoder.encode('{"type":"error","error":"stream failed"}\n'));
|
|
controller.close();
|
|
},
|
|
});
|
|
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue(
|
|
new Response(stream, {
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': 'application/x-ndjson',
|
|
},
|
|
}),
|
|
),
|
|
);
|
|
|
|
await expect(fetchGraph('big-repo')).rejects.toMatchObject({
|
|
message: 'stream failed',
|
|
});
|
|
});
|
|
});
|