diff --git a/graph-ui/src/architecture/SourceEvidenceDrawer.test.tsx b/graph-ui/src/architecture/SourceEvidenceDrawer.test.tsx index fa859c26e..d039fa5db 100644 --- a/graph-ui/src/architecture/SourceEvidenceDrawer.test.tsx +++ b/graph-ui/src/architecture/SourceEvidenceDrawer.test.tsx @@ -149,15 +149,18 @@ it('does not invent line evidence for a malformed source body', async () => { it('preserves the actual Module EOF line reported by the source API and highlights its call site', async () => { // Recorded local project_lock.c response, start_line=53/end_line=190. - // Its trailing empty segment is the indexed EOF line, not a page separator. + // Its trailing empty segment is the indexed EOF line, not a page separator: + // the range matches. The file itself ends at 189 (review of K13), so the + // drawer neither draws an empty line 190 nor names it in the range. const ui = await mount(vi.fn(async () => page(53, { ...projectLockEof, source_mode: 'full', source_truncated: undefined, source_clipped: undefined, next_start_line: undefined }))); await ui.render({ filePath: path, line: 61 }); expect(ui.container.querySelector('[role=alert]')).toBeNull(); - expect(ui.container.querySelectorAll('code > span')).toHaveLength(138); - expect(ui.container.querySelector('code > span:last-child .source-evidence-line-number')?.textContent).toBe('190'); + expect(ui.container.querySelectorAll('code > span')).toHaveLength(137); + expect(ui.container.querySelector('code > span:last-child .source-evidence-line-number')?.textContent).toBe('189'); expect(ui.container.querySelector('[data-selected=true]')?.textContent).toContain('cbm_private_lock_directory_close(directory)'); - expect(ui.container.textContent).toContain('Lines 53 to 190'); + expect(ui.container.textContent).toContain('Lines 53 to 189'); + expect(ui.container.textContent).toContain('end of file'); expect([...ui.container.querySelectorAll('button')].find(button => button.textContent === 'Next lines')?.disabled).toBe(true); }); @@ -188,7 +191,11 @@ it('says "end of file" when the last page reaches the end of the Module, so a di await act(async () => { await new Promise(resolve => setTimeout(resolve, 0)); }); const pager = ui.container.querySelector('nav[aria-label="Source pages"]')!; expect(ui.container.querySelector('[role=alert]')).toBeNull(); - expect(pager.textContent).toContain('Lines 42 to 66 · end of file'); + // Review of K13: the range, the lines drawn and the title all end at the file's real last line, 65. + expect(pager.textContent).toContain('Lines 42 to 65 · end of file'); + const numbers = [...ui.container.querySelectorAll('.source-evidence-line-number')].map(entry => entry.textContent); + expect(numbers).toHaveLength(24); + expect(numbers.at(-1)).toBe('65'); const next = [...pager.querySelectorAll('button')].find(button => button.textContent === 'Next lines')!; expect(next.disabled).toBe(true); // The title names the file's real last line, not the empty end segment the range counts. @@ -202,4 +209,6 @@ it('names the last line itself when a file ends without a final line break (hand await act(async () => { await new Promise(resolve => setTimeout(resolve, 0)); }); const next = [...ui.container.querySelectorAll('nav[aria-label="Source pages"] button')].find(button => button.textContent === 'Next lines')!; expect(next.getAttribute('title')).toBe('Line 66 is the last line of this file.'); + expect(ui.container.querySelector('nav[aria-label="Source pages"]')!.textContent).toContain('Lines 42 to 66 · end of file'); + expect([...ui.container.querySelectorAll('.source-evidence-line-number')].at(-1)?.textContent).toBe('66'); }); diff --git a/graph-ui/src/architecture/SourceEvidenceDrawer.tsx b/graph-ui/src/architecture/SourceEvidenceDrawer.tsx index e56b2da1d..8edf6502c 100644 --- a/graph-ui/src/architecture/SourceEvidenceDrawer.tsx +++ b/graph-ui/src/architecture/SourceEvidenceDrawer.tsx @@ -105,7 +105,10 @@ export default function SourceEvidenceDrawer({ project, target, graph, client, o const canNext = Boolean(more && validLine(next) && validLine(lastLine) && next === lastLine + 1 && rangeMatches); const atEnd = Boolean(source && !more && rangeMatches && validLine(lastLine)); // The Module range counts the empty segment after the final line break; the file's last line is the one before it. - const lastFileLine = atEnd && lines.length > 1 && lines.at(-1) === '' ? lastLine! - 1 : lastLine; + const endSegment = atEnd && lines.length > 1 && lines.at(-1) === ''; + const lastFileLine = endSegment ? lastLine! - 1 : lastLine; + // Review of K13: range text, drawn lines and title all stop at that line, not at the empty segment after it. + const shownLines = endSegment ? lines.slice(0, -1) : lines; const locationVisible = requestedLine !== undefined && validLine(firstLine) && validLine(lastLine) && requestedLine >= firstLine && requestedLine <= lastLine && rangeMatches; useEffect(() => { @@ -142,7 +145,7 @@ export default function SourceEvidenceDrawer({ project, target, graph, client, o {!rangeMatches &&
Source text does not match the reported line range. Line numbers and highlighting are unavailable.
} {requestedLine && !locationVisible && rangeMatches &&Requested line {requestedLine} is outside this returned window. {start === initialStart ? ' The index or requested source window may be stale.' : }
} -{lines.map((line, index) => {
+ {shownLines.map((line, index) => {
const number = (firstLine ?? 1) + index;
const selected = rangeMatches && number === requestedLine;
return
@@ -151,7 +154,7 @@ export default function SourceEvidenceDrawer({ project, target, graph, client, o
{lines.length === 0 && No source text was returned.
}
{more && !canNext && The source is truncated, but no valid next page was supplied. Refresh or open Explore to inspect the available source.
}
>}
;