|
4 | 4 | *--------------------------------------------------------------------------------------------*/ |
5 | 5 |
|
6 | 6 | import { describe, expect, it } from 'vitest'; |
| 7 | +import { DocumentId } from '../../../../platform/inlineEdits/common/dataTypes/documentId'; |
| 8 | +import { NoNextEditReason, StreamedEdit } from '../../../../platform/inlineEdits/common/statelessNextEditProvider'; |
7 | 9 | import { AsyncIterUtils } from '../../../../util/common/asyncIterableUtils'; |
| 10 | +import { StringText } from '../../../../util/vs/editor/common/core/text/abstractText'; |
8 | 11 | import { XtabCustomDiffPatchResponseHandler } from '../../node/xtabCustomDiffPatchResponseHandler'; |
9 | 12 |
|
| 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 | + |
10 | 27 | describe('XtabCustomDiffPatchResponseHandler', () => { |
11 | 28 |
|
12 | 29 | async function collectPatches(patchText: string): Promise<string> { |
@@ -101,4 +118,77 @@ another_file.js: |
101 | 118 | -Old line 2" |
102 | 119 | `); |
103 | 120 | }); |
| 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 | + }); |
104 | 194 | }); |
0 commit comments