diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 96d35d8f5..af229b38a 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -96,6 +96,9 @@ ### Fixed - Fixed the `/extensions` dashboard tab labeled "Agents (standard)" being confused with the `/agents` subagents feature — the `.agent`/`.agents` config-standard provider now presents as "Agent Dirs (.agent/.agents)" since it lists skills, rules, prompts, commands, and context/system files, never subagents ([#5821](https://github.com/can1357/oh-my-pi/issues/5821)). +### Fixed + +- Fixed non-raw `read` line selectors returning context outside the requested inclusive range ([#5802](https://github.com/can1357/oh-my-pi/issues/5802)). ## [17.0.2] - 2026-07-17 diff --git a/packages/coding-agent/src/tools/read.ts b/packages/coding-agent/src/tools/read.ts index 21cf26894..d99e42d60 100644 --- a/packages/coding-agent/src/tools/read.ts +++ b/packages/coding-agent/src/tools/read.ts @@ -54,7 +54,7 @@ import { } from "../session/streaming-output"; import { fileHyperlink, renderCodeCell, renderMarkdownCell, renderStatusLine, tryResolveInternalUrlSync } from "../tui"; import { CachedOutputBlock, markFramedBlockComponent } from "../tui/output-block"; -import { buildLineEntriesWithBlockContext, type LineEntry, lineEntriesToPlainText } from "../utils/block-context"; +import { buildLineEntries, type LineEntry, lineEntriesToPlainText } from "../utils/block-context"; import { resolveFileDisplayMode } from "../utils/file-display-mode"; import { ImageInputTooLargeError, @@ -164,7 +164,7 @@ const PROSE_SUMMARY_EXTENSIONS = new Set([".md", ".txt"]); // Remote mount path prefix (sshfs mounts) - skip fuzzy matching to avoid hangs const REMOTE_MOUNT_PREFIX = getRemoteDir() + path.sep; -async function readBracketContextFullLines(absolutePath: string, fileSize: number): Promise { +async function readSmallFileLines(absolutePath: string, fileSize: number): Promise { if (fileSize > SNAPSHOT_MAX_BYTES) return undefined; try { return normalizeToLF(await Bun.file(absolutePath).text()).split("\n"); @@ -395,40 +395,6 @@ function formatSummaryElisionFooter( } const READ_CHUNK_SIZE = 8 * 1024; -/** - * Context lines added around an explicit range read. Anchor-stale failures - * cluster on edits whose anchors land just outside the most recent read - * window, but the data (`scripts/session-stats/analyze_selector_reads.py`) - * shows most follow-up reads are disjoint hops, not adjacent extensions — - * so symmetric padding rarely pays for itself. - * - * Leading=1 catches accidental single-line reads where the anchor is the - * line immediately above the requested start. Trailing=3 buffers the - * common case where the agent asks for a narrow range and then needs the - * next few lines to disambiguate an anchor. - */ -const RANGE_LEADING_CONTEXT_LINES = 1; -const RANGE_TRAILING_CONTEXT_LINES = 3; - -/** - * Expand a [start, end) range with leading/trailing context lines on the - * sides where the user actually constrained the range. A start of 0 (no - * explicit offset) does not get leading context — that's already an - * open-ended read from the top. - */ -function expandRangeWithContext( - requestedStart: number, - requestedEnd: number, - totalLines: number, - expandStart: boolean, - expandEnd: boolean, -): { startLine: number; endLine: number } { - return { - startLine: expandStart ? Math.max(0, requestedStart - RANGE_LEADING_CONTEXT_LINES) : requestedStart, - endLine: expandEnd ? Math.min(totalLines, requestedEnd + RANGE_TRAILING_CONTEXT_LINES) : requestedEnd, - }; -} - async function streamLinesFromFile( filePath: string, startLine: number, @@ -1305,26 +1271,10 @@ export class ReadTool implements AgentTool { const details = options.details ?? {}; const allLines = text.split("\n"); const totalLines = allLines.length; - // User-requested 0-indexed range start. Lines BEFORE this are leading - // context (added below if offset is explicit). const requestedStart = offset ? Math.max(0, offset - 1) : 0; + const startLine = requestedStart; const ignoreResultLimits = options.ignoreResultLimits ?? false; - const requestedEnd = limit !== undefined ? Math.min(requestedStart + limit, allLines.length) : allLines.length; - // Expand only on sides the user actually constrained: leading context - // when offset>1, trailing context when a finite limit was set. Raw mode - // never expands — without line numbers the padding is indistinguishable - // from requested content, so `raw:31-31` must return line 31 and nothing - // else (verbatim-extraction contract). - const rawDisplay = options.raw === true; - const expanded = expandRangeWithContext( - requestedStart, - requestedEnd, - allLines.length, - !rawDisplay && offset !== undefined && offset > 1, - !rawDisplay && limit !== undefined, - ); - const startLine = expanded.startLine; - const endLineExpanded = expanded.endLine; + const endLine = limit !== undefined ? Math.min(startLine + limit, allLines.length) : allLines.length; const startLineDisplay = startLine + 1; const resultBuilder = toolResult(details); @@ -1350,7 +1300,6 @@ export class ReadTool implements AgentTool { .done(); } - const endLine = endLineExpanded; const selectedContent = allLines.slice(startLine, endLine).join("\n"); const userLimitedLines = limit !== undefined ? endLine - startLine : undefined; const truncation = ignoreResultLimits ? noTruncResult(selectedContent) : truncateHead(selectedContent); @@ -1395,10 +1344,8 @@ export class ReadTool implements AgentTool { emittedHashlineHeader = true; return prependHashlineHeader(formatted, hashContext); }; - const buildLineEntries = (endLineDisplay: number): LineEntry[] => - buildLineEntriesWithBlockContext(allLines, [{ startLine: startLineDisplay, endLine: endLineDisplay }], { - path: options.sourcePath, - }); + const buildSelectedLineEntries = (endLineDisplay: number): LineEntry[] => + buildLineEntries(allLines, [{ startLine: startLineDisplay, endLine: endLineDisplay }]); let outputText: string; let truncationInfo: @@ -1436,7 +1383,7 @@ export class ReadTool implements AgentTool { rawSeenLines = contiguousLineNumbers(startLineDisplay, outputLines); outputText = formatText(truncation.content, startLineDisplay); } else { - outputText = formatLineEntries(buildLineEntries(endLineDisplay), startLineDisplay); + outputText = formatLineEntries(buildSelectedLineEntries(endLineDisplay), startLineDisplay); } details.truncation = truncation; truncationInfo = { @@ -1451,7 +1398,7 @@ export class ReadTool implements AgentTool { rawSeenLines = contiguousLineNumbers(startLineDisplay, userLimitedLines); outputText = formatText(selectedContent, startLineDisplay); } else { - outputText = formatLineEntries(buildLineEntries(endLine), startLineDisplay); + outputText = formatLineEntries(buildSelectedLineEntries(endLine), startLineDisplay); } outputText += `\n\n[${remaining} more lines in ${options.entityLabel}. Use :${nextOffset} to continue]`; } else { @@ -1459,7 +1406,7 @@ export class ReadTool implements AgentTool { rawSeenLines = contiguousLineNumbers(startLineDisplay, endLine - startLine); outputText = formatText(truncation.content, startLineDisplay); } else { - outputText = formatLineEntries(buildLineEntries(endLine), startLineDisplay); + outputText = formatLineEntries(buildSelectedLineEntries(endLine), startLineDisplay); } } @@ -1480,8 +1427,8 @@ export class ReadTool implements AgentTool { * Render a multi-range read against in-memory text. Each range emits a * formatted block with its own anchors / line numbers, blocks are joined * with an elision separator, and ranges past EOF surface as `[…]` notices - * so the model can correct the next call. No leading/trailing context is - * added — multi-range callers always specify exact bounds. + * so the model can correct the next call. Context is never added because + * multi-range callers always specify exact bounds. */ #buildInMemoryMultiRangeResult( text: string, @@ -1538,7 +1485,7 @@ export class ReadTool implements AgentTool { if (options.raw === true) { outputText = rawParts.length > 0 ? rawParts.join("\n\n…\n\n") : ""; } else if (visibleSpans.length > 0) { - const entries = buildLineEntriesWithBlockContext(allLines, visibleSpans, { path: options.sourcePath }); + const entries = buildLineEntries(allLines, visibleSpans); if (shouldAddHashLines) seenLines = lineNumbersFromEntries(entries); const firstLine = entries.find(entry => entry.kind === "line"); if (firstLine?.kind === "line") { @@ -1622,7 +1569,7 @@ export class ReadTool implements AgentTool { const notices: string[] = []; const visibleSpans: Array<{ startLine: number; endLine: number }> = []; const displayLineByNumber = new Map(); - const fullLines = rawSelector ? undefined : await readBracketContextFullLines(absolutePath, fileSize); + const fullLines = rawSelector ? undefined : await readSmallFileLines(absolutePath, fileSize); let columnTruncated = 0; let displayContent: { text: string; startLine: number; lineNumbers?: Array } | undefined; @@ -1690,23 +1637,9 @@ export class ReadTool implements AgentTool { let outputText: string; if (!rawSelector && fullLines && visibleSpans.length > 0) { - const entries = buildLineEntriesWithBlockContext( - fullLines, - visibleSpans, - { path: absolutePath }, - { - lineText: (lineNumber, sourceText) => { - const visibleText = displayLineByNumber.get(lineNumber); - if (visibleText !== undefined) return visibleText; - if (maxColumns <= 0) return sourceText; - const truncated = truncateLine(sourceText, maxColumns); - if (truncated.wasTruncated) { - columnTruncated = maxColumns; - } - return truncated.text; - }, - }, - ); + const entries = buildLineEntries(fullLines, visibleSpans, { + lineText: (lineNumber, sourceText) => displayLineByNumber.get(lineNumber) ?? sourceText, + }); const firstLine = entries.find(entry => entry.kind === "line"); displayContent = { text: lineEntriesToPlainText(entries, BRACKET_CONTEXT_ELLIPSIS), @@ -2513,7 +2446,7 @@ export class ReadTool implements AgentTool { // Raw text or line-range mode const { offset, limit } = selToOffsetLimit(parsed); // Try ACP bridge first — editor's in-memory buffer is source of truth. - // Request full text so local range rendering keeps normal context and line numbers. + // Request full text so local range rendering preserves exact bounds and line numbers. const bridgePromise = this.#routeReadThroughBridge(absolutePath); if (bridgePromise !== undefined) { try { @@ -2535,24 +2468,15 @@ export class ReadTool implements AgentTool { } } - // User-requested 0-indexed range start. Lines BEFORE this become - // leading context (added below if offset is explicit). Raw mode - // never adds context: without line numbers the padding is - // indistinguishable from requested content, so `raw:31-31` must - // return line 31 and nothing else. const rawSelector = isRawSelector(parsed); const requestedStart = offset ? Math.max(0, offset - 1) : 0; - const expandStart = !rawSelector && offset !== undefined && offset > 1; - const expandEnd = !rawSelector && limit !== undefined; - const leadingContext = expandStart ? Math.min(requestedStart, RANGE_LEADING_CONTEXT_LINES) : 0; - const trailingContext = expandEnd ? RANGE_TRAILING_CONTEXT_LINES : 0; - const startLine = requestedStart - leadingContext; + const startLine = requestedStart; const startLineDisplay = startLine + 1; const DEFAULT_LIMIT = this.#defaultLimit; const effectiveLimit = limit ?? DEFAULT_LIMIT; - const maxLinesToCollect = Math.min(effectiveLimit + leadingContext + trailingContext, DEFAULT_MAX_LINES); - const selectedLineLimit = effectiveLimit + leadingContext + trailingContext; + const maxLinesToCollect = Math.min(effectiveLimit, DEFAULT_MAX_LINES); + const selectedLineLimit = effectiveLimit; // Scale byte budget with line limit so the configured line count actually fits. // Assume ~512 bytes/line average; never go below the shared default. const maxBytesForRead = Math.max(DEFAULT_MAX_BYTES, maxLinesToCollect * 512); @@ -2614,15 +2538,6 @@ export class ReadTool implements AgentTool { if (cloned) displayLines = cloned; } - const displayLineByNumber = new Map(); - for (let i = 0; i < displayLines.length; i++) { - displayLineByNumber.set(startLineDisplay + i, displayLines[i] ?? ""); - } - const bracketContextFullLines = rawSelector - ? undefined - : await readBracketContextFullLines(absolutePath, fileSize); - const displayedEndLine = startLineDisplay + Math.max(0, displayLines.length - 1); - const selectedContent = displayLines.join("\n"); const userLimitedLines = collectedLines.length; @@ -2679,36 +2594,6 @@ export class ReadTool implements AgentTool { emittedHashlineHeader = true; return prependHashlineHeader(formatted, hashContext); }; - const formatBracketAwareText = (): string | undefined => { - if (!bracketContextFullLines) return undefined; - const entries = buildLineEntriesWithBlockContext( - bracketContextFullLines, - [{ startLine: startLineDisplay, endLine: displayedEndLine }], - { path: absolutePath }, - { - lineText: (lineNumber, sourceText) => { - const visibleText = displayLineByNumber.get(lineNumber); - if (visibleText !== undefined) return visibleText; - if (maxColumns <= 0) return sourceText; - const truncated = truncateLine(sourceText, maxColumns); - if (truncated.wasTruncated) { - columnTruncated = maxColumns; - } - return truncated.text; - }, - }, - ); - const firstLine = entries.find(entry => entry.kind === "line"); - capturedDisplayContent = { - text: lineEntriesToPlainText(entries, BRACKET_CONTEXT_ELLIPSIS), - startLine: firstLine?.kind === "line" ? firstLine.lineNumber : startLineDisplay, - lineNumbers: entries.map(entry => (entry.kind === "line" ? entry.lineNumber : null)), - }; - const formatted = formatLineEntriesWithMode(entries, shouldAddHashLines, shouldAddLineNumbers); - if (!hashContext || emittedHashlineHeader) return formatted; - emittedHashlineHeader = true; - return prependHashlineHeader(formatted, hashContext); - }; let outputText: string; @@ -2739,7 +2624,7 @@ export class ReadTool implements AgentTool { }, }; } else if (truncation.truncated) { - outputText = formatBracketAwareText() ?? formatText(truncation.content, startLineDisplay); + outputText = formatText(truncation.content, startLineDisplay); details = { truncation }; sourcePath = absolutePath; truncationInfo = { @@ -2753,7 +2638,7 @@ export class ReadTool implements AgentTool { } else if (startLine + userLimitedLines < totalFileLines || !reachedEof) { const nextOffset = startLine + userLimitedLines + 1; - outputText = formatBracketAwareText() ?? formatText(truncation.content, startLineDisplay); + outputText = formatText(truncation.content, startLineDisplay); outputText += reachedEof ? `\n\n[${totalFileLines - (startLine + userLimitedLines)} more lines in file. Use :${nextOffset} to continue]` : `\n\n[More lines in file (${formatBytes(fileSize)} total; not scanned to EOF). Use :${nextOffset} to continue]`; @@ -2761,7 +2646,7 @@ export class ReadTool implements AgentTool { sourcePath = absolutePath; } else { // No truncation, no user limit exceeded - outputText = formatBracketAwareText() ?? formatText(truncation.content, startLineDisplay); + outputText = formatText(truncation.content, startLineDisplay); details = {}; sourcePath = absolutePath; } @@ -2981,16 +2866,11 @@ export class ReadTool implements AgentTool { const { offset, limit } = selToOffsetLimit(parsedSel); const requestedStart = offset ? Math.max(0, offset - 1) : 0; - // Raw mode never adds context lines — see the plain-file range path. - const expandStart = !rawSelector && offset !== undefined && offset > 1; - const expandEnd = !rawSelector && limit !== undefined; - const leadingContext = expandStart ? Math.min(requestedStart, RANGE_LEADING_CONTEXT_LINES) : 0; - const trailingContext = expandEnd ? RANGE_TRAILING_CONTEXT_LINES : 0; - const startLine = requestedStart - leadingContext; + const startLine = requestedStart; const startLineDisplay = startLine + 1; const effectiveLimit = limit ?? this.#defaultLimit; - const maxLinesToCollect = Math.min(effectiveLimit + leadingContext + trailingContext, DEFAULT_MAX_LINES); - const selectedLineLimit = effectiveLimit + leadingContext + trailingContext; + const maxLinesToCollect = Math.min(effectiveLimit, DEFAULT_MAX_LINES); + const selectedLineLimit = effectiveLimit; const maxBytesForRead = Math.max(DEFAULT_MAX_BYTES, maxLinesToCollect * 512); const streamResult = await streamLinesFromFile( artifact.path, diff --git a/packages/coding-agent/src/utils/block-context.ts b/packages/coding-agent/src/utils/block-context.ts index 5b450cf97..045d5103c 100644 --- a/packages/coding-agent/src/utils/block-context.ts +++ b/packages/coding-agent/src/utils/block-context.ts @@ -266,27 +266,24 @@ export function findBlockContextLines( return nativeBlockContext(fullLines, visible, source) ?? lexicalBracketContext(fullLines, visible); } -/** - * Build display entries for `visibleSpans` plus any off-window block-boundary - * lines, in source order, with `{ kind: "ellipsis" }` markers inserted across - * non-contiguous gaps. `options.lineText` lets callers substitute display text - * (e.g. column-truncated lines) for a given line number. - */ -export function buildLineEntriesWithBlockContext( - fullLines: readonly string[], - visibleSpans: readonly LineSpan[], - source: BlockContextSource = {}, - options: { - lineText?: (lineNumber: number, sourceText: string, context: boolean) => string; - } = {}, -): LineEntry[] { - const spans = normalizeLineSpans(visibleSpans, fullLines.length); - const visible = visibleLineNumbers(spans); - const context = findBlockContextLines(fullLines, visible, source); - const allLines = new Set(visible); - for (const lineNumber of context.keys()) allLines.add(lineNumber); +interface LineEntryOptions { + lineText?: (lineNumber: number, sourceText: string, context: boolean) => string; +} + +function buildEntries( + fullLines: readonly string[], + visible: ReadonlySet, + context: ReadonlyMap | undefined, + options: LineEntryOptions, +): LineEntry[] { + const sorted = [...visible]; + if (context) { + for (const lineNumber of context.keys()) { + if (!visible.has(lineNumber)) sorted.push(lineNumber); + } + } + sorted.sort((left, right) => left - right); - const sorted = [...allLines].sort((left, right) => left - right); const entries: LineEntry[] = []; let previousLine: number | undefined; for (const lineNumber of sorted) { @@ -294,7 +291,7 @@ export function buildLineEntriesWithBlockContext( entries.push({ kind: "ellipsis" }); } const sourceText = fullLines[lineNumber - 1] ?? ""; - const isContext = context.has(lineNumber); + const isContext = context?.has(lineNumber) === true; entries.push({ kind: "line", lineNumber, @@ -307,6 +304,33 @@ export function buildLineEntriesWithBlockContext( return entries; } +/** Build display entries for exactly the requested spans, separated by ellipses. */ +export function buildLineEntries( + fullLines: readonly string[], + visibleSpans: readonly LineSpan[], + options: LineEntryOptions = {}, +): LineEntry[] { + const spans = normalizeLineSpans(visibleSpans, fullLines.length); + return buildEntries(fullLines, visibleLineNumbers(spans), undefined, options); +} + +/** + * Build display entries for `visibleSpans` plus any off-window block-boundary + * lines, in source order, with `{ kind: "ellipsis" }` markers inserted across + * non-contiguous gaps. `options.lineText` lets callers substitute display text + * (e.g. column-truncated lines) for a given line number. + */ +export function buildLineEntriesWithBlockContext( + fullLines: readonly string[], + visibleSpans: readonly LineSpan[], + source: BlockContextSource = {}, + options: LineEntryOptions = {}, +): LineEntry[] { + const spans = normalizeLineSpans(visibleSpans, fullLines.length); + const visible = visibleLineNumbers(spans); + return buildEntries(fullLines, visible, findBlockContextLines(fullLines, visible, source), options); +} + export function lineEntriesToPlainText(entries: readonly LineEntry[], ellipsis = "…"): string { return entries.map(entry => (entry.kind === "ellipsis" ? ellipsis : entry.text)).join("\n"); } diff --git a/packages/coding-agent/test/read-multi-range.test.ts b/packages/coding-agent/test/read-multi-range.test.ts index f85a1da33..8b2abee49 100644 --- a/packages/coding-agent/test/read-multi-range.test.ts +++ b/packages/coding-agent/test/read-multi-range.test.ts @@ -79,6 +79,8 @@ describe("read tool multi-range selector", () => { expect(text).toContain("line 20"); expect(text).toContain("line 21"); expect(text).toContain("line 22"); + expect(text).not.toMatch(/^2:line 2$/m); + expect(text).not.toMatch(/^6:line 6$/m); // Lines between the ranges must be elided expect(text).not.toContain("line 10"); expect(text).not.toContain("line 19"); @@ -86,7 +88,7 @@ describe("read tool multi-range selector", () => { expect(text).toContain("…"); }); - it("includes the matching closing bracket line outside a forward range", async () => { + it("does not add a closing bracket outside a forward range", async () => { const filePath = path.join(tmpDir, "brackets.ts"); await fs.writeFile( filePath, @@ -106,13 +108,13 @@ describe("read tool multi-range selector", () => { const text = textOutput(await tool.execute("call-bracket-close", { path: `${filePath}:1-1` })); expect(text).toContain("function outer() {"); - expect(text).toContain("…"); - expect(text).toContain("}"); + expect(text).not.toContain("…"); + expect(text).not.toMatch(/^7:}$/m); expect(text).not.toContain("const four"); expect(text).not.toContain("return one + two"); }); - it("includes the matching opening bracket line outside a reverse range", async () => { + it("does not add an opening bracket outside a reverse range", async () => { const filePath = path.join(tmpDir, "brackets.ts"); await fs.writeFile( filePath, @@ -131,13 +133,14 @@ describe("read tool multi-range selector", () => { const tool = new ReadTool(createSession(tmpDir)); const text = textOutput(await tool.execute("call-bracket-open", { path: `${filePath}:7-7` })); - expect(text.indexOf("function outer() {")).toBeLessThan(text.indexOf("}")); - expect(text).toContain("…"); + expect(text).toMatch(/^7:}$/m); + expect(text).not.toContain("function outer() {"); + expect(text).not.toContain("…"); expect(text).not.toContain("const one = 1"); expect(text).not.toContain("const four = 4"); }); - it("uses tree-sitter syntactic spans for indentation languages (Python)", async () => { + it("does not add Python syntactic boundaries outside a range", async () => { const filePath = path.join(tmpDir, "module.py"); await fs.writeFile( filePath, @@ -156,15 +159,11 @@ describe("read tool multi-range selector", () => { ); const tool = new ReadTool(createSession(tmpDir)); - // Read only the `def` header (expands by a few trailing context lines). - // Python has no closing delimiter, so a bracket scan would surface - // nothing; tree-sitter surfaces the def's last body line (9) as the - // block boundary, behind an ellipsis for the skipped middle. const text = textOutput(await tool.execute("call-py-def", { path: `${filePath}:1-1` })); expect(text).toContain("def greet(name):"); - expect(text).toContain("…"); - expect(text).toContain("return a + b + c + d + e + f + g + len(name)"); + expect(text).not.toContain("…"); + expect(text).not.toContain("return a + b + c + d + e + f + g + len(name)"); expect(text).not.toContain("trailing = 1"); }); @@ -179,7 +178,7 @@ describe("read tool multi-range selector", () => { // All lines from the merged range present for (const i of [3, 4, 5, 6, 7, 8, 9]) { - expect(text).toContain(`line ${i}\n`); + expect(text).toMatch(new RegExp(`^${i}:line ${i}$`, "m")); } // No separator because ranges merged into one contiguous block expect(text).not.toContain("…"); diff --git a/packages/coding-agent/test/tools/read-artifact-large.test.ts b/packages/coding-agent/test/tools/read-artifact-large.test.ts index 46831a30f..8c65d5b9b 100644 --- a/packages/coding-agent/test/tools/read-artifact-large.test.ts +++ b/packages/coding-agent/test/tools/read-artifact-large.test.ts @@ -77,6 +77,7 @@ describe("read tool large artifact handling", () => { expect(output).toContain("line-001"); expect(output).toContain("line-003"); + expect(output).not.toContain("line-004"); expect(output).toContain("Artifact storage:"); expect(output).toContain("artifact://0:raw:N-M"); expect(output).not.toContain("line-400"); diff --git a/packages/coding-agent/test/tools/read-pdf-line-range.test.ts b/packages/coding-agent/test/tools/read-pdf-line-range.test.ts index a1cb17321..d3c0d3611 100644 --- a/packages/coding-agent/test/tools/read-pdf-line-range.test.ts +++ b/packages/coding-agent/test/tools/read-pdf-line-range.test.ts @@ -137,6 +137,7 @@ describe("read PDF with a line-range selector", () => { .join("\n"); expect(selectorText).toContain("pdf line 2"); expect(selectorText).toContain("pdf line 3"); + expect(selectorText).not.toContain("pdf line 1"); expect(convert).toHaveBeenCalledTimes(1); } finally { diff --git a/packages/coding-agent/test/tools/read-raw-range.test.ts b/packages/coding-agent/test/tools/read-raw-range.test.ts index 75d5c3cc9..d1552a91b 100644 --- a/packages/coding-agent/test/tools/read-raw-range.test.ts +++ b/packages/coding-agent/test/tools/read-raw-range.test.ts @@ -59,14 +59,12 @@ describe("read tool raw range exactness", () => { expect(output.trimEnd()).toBe("L01\nL02"); }); - it("keeps context padding for numbered range reads", async () => { - // Numbered mode intentionally pads (leading anchor buffer + trailing - // disambiguation lines) — line numbers make the padding self-describing. + it("returns exactly the requested numbered range without context padding", async () => { const result = await tool.execute("call-numbered", { path: `${filePath}:31-31` }); const output = getTextOutput(result); expect(output).toContain("L31"); - expect(output).toContain("L30"); - expect(output).toContain("L32"); + expect(output).not.toContain("L30"); + expect(output).not.toContain("L32"); }); });