diff --git a/graph-ui/src/App.tsx b/graph-ui/src/App.tsx index c297a65de..40530a59b 100644 --- a/graph-ui/src/App.tsx +++ b/graph-ui/src/App.tsx @@ -156,7 +156,6 @@ import { CbmRpcProvider, symbolKindOf } from './provider/cbm-rpc-provider'; import MonacoReader from './reader/MonacoReader'; import type { ReaderStatus } from './reader/MonacoReader'; import { FileNotReadableError, loadFileDocument, READER_RPC_TOOL } from './reader/file-source'; -import { reportError } from './provider/error-observer'; import type { ReaderDocument } from './reader/file-source'; import { badgesForLines } from './core/step-badge-decorator'; import type { SymbolRef } from './core/focus-protocol'; @@ -1760,12 +1759,6 @@ export default function App(): JSX.Element { setDocument(undefined); setReaderStatus(error instanceof FileNotReadableError ? 'unavailable' : 'failed'); setReaderMessage(error instanceof Error ? error.message : String(error)); - // The reader shows the sentence; the frontend log keeps it - // (the /rpc failures underneath announce themselves, a file - // the index does not know does not). - if (error instanceof FileNotReadableError) { - reportError({ source: 'reader', level: 'warn', message: error.message }); - } }); }, [client, project], diff --git a/graph-ui/src/reader/file-source.test.ts b/graph-ui/src/reader/file-source.test.ts index edc5281f7..a3917c116 100644 --- a/graph-ui/src/reader/file-source.test.ts +++ b/graph-ui/src/reader/file-source.test.ts @@ -9,6 +9,8 @@ * rpc-transport und rpc-client hindurch; ersetzt ist nur die Leitung. */ import { describe, expect, it } from 'vitest'; +import { observeErrors } from '../provider/error-observer'; +import type { ErrorReport } from '../provider/error-observer'; import { RpcIntelligenceClient } from '../provider/rpc-client'; import { FakeRpc, queryContains, rowsText } from '../test-support/rpc-recordings'; import type { Route } from '../test-support/rpc-recordings'; @@ -232,6 +234,51 @@ describe('loadFileDocument, seit dem schlanken Vertrag (#1597)', () => { }); }); +describe('loadFileDocument announces to the frontend log', () => { + it('a file without a module node, and the server placeholder, before it throws', async () => { + const reports: ErrorReport[] = []; + const stop = observeErrors((report) => reports.push(report)); + try { + const missing = clientFor([ + noModuleRoute, + { tool: 'query_graph', when: queryContains('MATCH (n:File)'), text: rowsText(FILE_COLUMNS, []) }, + ]); + await expect(loadFileDocument(missing.client, PROJECT, 'assets/logo.svg')) + .rejects.toBeInstanceOf(FileNotReadableError); + + const gone = clientFor([ + moduleRoute('probe-small.src.ui.http_server', 2244), + snippetRoute({ start_line: 1, end_line: 500, source_mode: 'full', source: '(source not available)' }), + ]); + await expect(loadFileDocument(gone.client, PROJECT, 'src/ui/http_server.c')) + .rejects.toBeInstanceOf(FileNotReadableError); + } finally { + stop(); + } + expect(reports.map((report) => [report.source, report.level])).toEqual([ + ['reader', 'warn'], + ['reader', 'warn'], + ]); + expect(reports[0]?.message).toContain('The index has no module node for assets/logo.svg'); + expect(reports[1]?.message).toContain('could not read src/ui/http_server.c'); + }); + + it('nothing when the file loads', async () => { + const reports: ErrorReport[] = []; + const stop = observeErrors((report) => reports.push(report)); + try { + const { client } = clientFor([ + moduleRoute('probe-small.src.types', 3), + snippetRoute({ start_line: 1, end_line: 3, source_mode: 'full', source: 'a\nb\nc' }), + ]); + await loadFileDocument(client, PROJECT, 'src/types.ts'); + } finally { + stop(); + } + expect(reports).toEqual([]); + }); +}); + describe('truncationNoteFor', () => { it('nennt Zeilen, Deckel und den Grund, warum nicht nachgeladen wird', () => { diff --git a/graph-ui/src/reader/file-source.ts b/graph-ui/src/reader/file-source.ts index 3a82e2128..631e77c34 100644 --- a/graph-ui/src/reader/file-source.ts +++ b/graph-ui/src/reader/file-source.ts @@ -30,6 +30,7 @@ import { fileNodeForPath, moduleForFile, COLUMNS } from '../provider/cypher'; import type { RpcIntelligenceClient } from '../provider/rpc-client'; import type { CodeSnippetResult } from '../provider/rpc-schemas'; import { moduleQualifiedName, moduleQnFromFileQn, normalizeWorkspacePath } from '../app/module-qn'; +import { reportError } from '../provider/error-observer'; /** Das Werkzeug, aus dem der Inhalt des Readers kommt. Der Beweislauf schreibt es mit. */ export const READER_RPC_TOOL = 'get_code_snippet'; @@ -74,6 +75,18 @@ export class FileNotReadableError extends Error { } } +/** + * A file the reader cannot show is announced to the frontend log before it + * is thrown (provider/error-observer.ts). The /rpc failures underneath + * announce themselves; a file the index does not know, or the server's + * placeholder for a file gone from disk, is a successful call with nothing + * in it, and only this module knows that it is a failure. + */ +function notReadable(path: string, message: string): FileNotReadableError { + reportError({ source: 'reader', level: 'warn', message }); + return new FileNotReadableError(path, message); +} + function toOptionalNumber(cell: string | undefined): number | undefined { if (cell === undefined || cell === '') { return undefined; @@ -164,7 +177,7 @@ export async function loadFileDocument( const node = await lookupModuleNode(client, project, path); if (node === undefined) { - throw new FileNotReadableError( + throw notReadable( path, `The index has no module node for ${path}. This server delivers file content only through get_code_snippet on an indexed symbol, so a file the index did not record cannot be read here.`, ); @@ -173,7 +186,7 @@ export async function loadFileDocument( const qualifiedName = node.qualifiedName; const snippet = await client.getCodeSnippet(project, qualifiedName); if (snippet.source.length === 0 || snippet.source === SOURCE_UNAVAILABLE) { - throw new FileNotReadableError( + throw notReadable( path, snippet.source === SOURCE_UNAVAILABLE ? `The server could not read ${path} from the repository (${qualifiedName}): `