From 0fe968a3432187dbb293902940d7571bb005f1eb Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Tue, 8 Sep 2026 01:30:38 +0200 Subject: [PATCH] fix(ui): the reader announces a file it cannot show, where it is tested The previous commit announced the reader's failure from App.tsx's catch, with no test on that wiring, and its message claimed the case had been seen live. It had not: the live run covered console lines, the start line, the failures of an unknown project through the seam (/rpc tool errors and a /api 404), the ring mirror and the panel. This commit puts the announcement where the failure is decided, in loadFileDocument, for both of its shapes (no module node in the index; the server's "(source not available)" placeholder for a file gone from disk), and pins it with two reader tests: both shapes reach the observer as source "reader", level warn, and a file that loads announces nothing. Signed-off-by: Martin Vogel --- graph-ui/src/App.tsx | 7 ---- graph-ui/src/reader/file-source.test.ts | 47 +++++++++++++++++++++++++ graph-ui/src/reader/file-source.ts | 17 +++++++-- 3 files changed, 62 insertions(+), 9 deletions(-) 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}): `