Skip to content

Commit 9a12b5d

Browse files
authored
Merge pull request #4869 from microsoft/ulugbekna/nes-dont-yield-fetch-with-error-candidate
nes: fix: don't yield edits when fetch errors or is cancelled
2 parents 239f152 + e6efcff commit 9a12b5d

3 files changed

Lines changed: 97 additions & 0 deletions

File tree

src/extension/xtab/node/xtabCustomDiffPatchResponseHandler.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,15 @@ export class XtabCustomDiffPatchResponseHandler {
6969
workspaceRoot: URI | undefined,
7070
window: OffsetRange | undefined,
7171
originalWindow?: OffsetRange,
72+
getFetchFailure?: () => NoNextEditReason | undefined,
7273
): AsyncGenerator<StreamedEdit, NoNextEditReason, void> {
7374
const activeDocRelativePath = toUniquePath(activeDocumentId, workspaceRoot?.path);
7475
try {
7576
for await (const edit of XtabCustomDiffPatchResponseHandler.extractEdits(linesStream)) {
77+
const fetchFailure = getFetchFailure?.();
78+
if (fetchFailure) {
79+
return fetchFailure;
80+
}
7681
const targetDocument = edit.filePath === activeDocRelativePath
7782
? activeDocumentId
7883
: XtabCustomDiffPatchResponseHandler.resolveTargetDocument(edit.filePath, workspaceRoot) ?? activeDocumentId; // FIXME@ulugbekna: it's wrong to fallback to active document but just ignoring edits is also bad

src/extension/xtab/node/xtabProvider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -850,6 +850,8 @@ export class XtabProvider implements IStatelessNextEditProvider {
850850
activeDoc.id,
851851
activeDoc.workspaceRoot,
852852
pseudoEditWindow,
853+
undefined,
854+
() => chatResponseFailure ? mapChatFetcherErrorToNoNextEditReason(chatResponseFailure) : undefined,
853855
);
854856
} else if (opts.responseFormat === xtabPromptOptions.ResponseFormat.UnifiedWithXml) {
855857
const linesIter = linesStream[Symbol.asyncIterator]();

src/extension/xtab/test/node/xtabCustomDiffPatchResponseHandler.spec.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,26 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { describe, expect, it } from 'vitest';
7+
import { DocumentId } from '../../../../platform/inlineEdits/common/dataTypes/documentId';
8+
import { NoNextEditReason, StreamedEdit } from '../../../../platform/inlineEdits/common/statelessNextEditProvider';
79
import { AsyncIterUtils } from '../../../../util/common/asyncIterableUtils';
10+
import { StringText } from '../../../../util/vs/editor/common/core/text/abstractText';
811
import { XtabCustomDiffPatchResponseHandler } from '../../node/xtabCustomDiffPatchResponseHandler';
912

13+
async function consumeHandleResponse(
14+
...args: Parameters<typeof XtabCustomDiffPatchResponseHandler.handleResponse>
15+
): Promise<{ edits: StreamedEdit[]; returnValue: NoNextEditReason }> {
16+
const gen = XtabCustomDiffPatchResponseHandler.handleResponse(...args);
17+
const edits: StreamedEdit[] = [];
18+
for (; ;) {
19+
const result = await gen.next();
20+
if (result.done) {
21+
return { edits, returnValue: result.value };
22+
}
23+
edits.push(result.value);
24+
}
25+
}
26+
1027
describe('XtabCustomDiffPatchResponseHandler', () => {
1128

1229
async function collectPatches(patchText: string): Promise<string> {
@@ -101,4 +118,77 @@ another_file.js:
101118
-Old line 2"
102119
`);
103120
});
121+
122+
it('stops yielding edits when getFetchFailure returns a failure', async () => {
123+
const patchText = `file.ts:0
124+
-old
125+
+new
126+
file.ts:5
127+
-another old
128+
+another new`;
129+
const linesStream = AsyncIterUtils.fromArray(patchText.split('\n'));
130+
const docId = DocumentId.create('file:///file.ts');
131+
const documentBeforeEdits = new StringText('old\n');
132+
133+
let yieldCount = 0;
134+
const cancellationReason = new NoNextEditReason.GotCancelled('afterFetchCall');
135+
136+
const { edits, returnValue } = await consumeHandleResponse(
137+
linesStream,
138+
documentBeforeEdits,
139+
docId,
140+
undefined,
141+
undefined,
142+
undefined,
143+
() => {
144+
// Signal failure after the first edit has been yielded
145+
if (yieldCount++ > 0) {
146+
return cancellationReason;
147+
}
148+
return undefined;
149+
},
150+
);
151+
152+
expect(edits).toHaveLength(1);
153+
expect(returnValue).toBe(cancellationReason);
154+
});
155+
156+
it('does not yield truncated patch when fetch is cancelled mid-stream', async () => {
157+
// Full response would be:
158+
// file.ts:0 / -old / +new / file.ts:5 / -another old / +another new / +one more new
159+
// But the fetch is cancelled right before "+one more new" is emitted,
160+
// so the stream only delivers lines up to "+another new".
161+
// The second patch is incomplete and must not be yielded.
162+
const truncatedPatchText = `file.ts:0
163+
-old
164+
+new
165+
file.ts:5
166+
-another old
167+
+another new`;
168+
// "+one more new" is missing — fetch was cancelled before it arrived
169+
const linesStream = AsyncIterUtils.fromArray(truncatedPatchText.split('\n'));
170+
const docId = DocumentId.create('file:///file.ts');
171+
const documentBeforeEdits = new StringText('old\n');
172+
173+
let checkCount = 0;
174+
const cancellationReason = new NoNextEditReason.GotCancelled('afterFetchCall');
175+
176+
const { edits, returnValue } = await consumeHandleResponse(
177+
linesStream,
178+
documentBeforeEdits,
179+
docId,
180+
undefined,
181+
undefined,
182+
undefined,
183+
() => {
184+
// Fetch was still running (not yet cancelled) when the first edit was checked,
185+
// but was cancelled before the second (incomplete) edit is about to be yielded.
186+
return checkCount++ > 0 ? cancellationReason : undefined;
187+
},
188+
);
189+
190+
// The first (complete) edit is yielded; the second (truncated) edit is not
191+
expect(edits).toHaveLength(1);
192+
expect(returnValue).toBe(cancellationReason);
193+
});
104194
});

0 commit comments

Comments
 (0)