|
3 | 3 | * Licensed under the MIT License. See License.txt in the project root for license information. |
4 | 4 | *--------------------------------------------------------------------------------------------*/ |
5 | 5 |
|
6 | | -import { describe, expect, it } from 'vitest'; |
| 6 | +import { beforeEach, describe, expect, it } from 'vitest'; |
| 7 | +import { DocumentId } from '../../../../platform/inlineEdits/common/dataTypes/documentId'; |
| 8 | +import { NoNextEditReason, StreamedEdit } from '../../../../platform/inlineEdits/common/statelessNextEditProvider'; |
| 9 | +import { TestLogService } from '../../../../platform/testing/common/testLogService'; |
7 | 10 | import { AsyncIterUtils } from '../../../../util/common/asyncIterableUtils'; |
| 11 | +import { Position } from '../../../../util/vs/editor/common/core/position'; |
| 12 | +import { StringText } from '../../../../util/vs/editor/common/core/text/abstractText'; |
| 13 | +import { ensureDependenciesAreSet } from '../../../../util/vs/editor/common/core/text/positionToOffset'; |
| 14 | +import { CurrentDocument } from '../../common/xtabCurrentDocument'; |
8 | 15 | import { XtabCustomDiffPatchResponseHandler } from '../../node/xtabCustomDiffPatchResponseHandler'; |
9 | 16 |
|
| 17 | +async function consumeHandleResponse( |
| 18 | + ...args: Parameters<typeof XtabCustomDiffPatchResponseHandler.handleResponse> |
| 19 | +): Promise<{ edits: StreamedEdit[]; returnValue: NoNextEditReason }> { |
| 20 | + const gen = XtabCustomDiffPatchResponseHandler.handleResponse(...args); |
| 21 | + const edits: StreamedEdit[] = []; |
| 22 | + for (; ;) { |
| 23 | + const result = await gen.next(); |
| 24 | + if (result.done) { |
| 25 | + return { edits, returnValue: result.value }; |
| 26 | + } |
| 27 | + edits.push(result.value); |
| 28 | + } |
| 29 | +} |
| 30 | + |
10 | 31 | describe('XtabCustomDiffPatchResponseHandler', () => { |
11 | 32 |
|
| 33 | + beforeEach(() => { |
| 34 | + ensureDependenciesAreSet(); |
| 35 | + }); |
| 36 | + |
12 | 37 | async function collectPatches(patchText: string): Promise<string> { |
13 | 38 | const linesStream = AsyncIterUtils.fromArray(patchText.split('\n')); |
14 | 39 | const patches = await AsyncIterUtils.toArray(XtabCustomDiffPatchResponseHandler.extractEdits(linesStream)); |
@@ -113,4 +138,71 @@ another_file.js: |
113 | 138 | +Old line 1" |
114 | 139 | `); |
115 | 140 | }); |
| 141 | + |
| 142 | + it('stops yielding edits when getFetchFailure returns a failure', async () => { |
| 143 | + const patchText = `/file.ts:0 |
| 144 | +-old |
| 145 | ++new |
| 146 | +/file.ts:5 |
| 147 | +-another old |
| 148 | ++another new`; |
| 149 | + const linesStream = AsyncIterUtils.fromArray(patchText.split('\n')); |
| 150 | + const docId = DocumentId.create('file:///file.ts'); |
| 151 | + const documentBeforeEdits = new CurrentDocument(new StringText('old\n'), new Position(1, 1)); |
| 152 | + |
| 153 | + let yieldCount = 0; |
| 154 | + const cancellationReason = new NoNextEditReason.GotCancelled('afterFetchCall'); |
| 155 | + |
| 156 | + const { edits, returnValue } = await consumeHandleResponse( |
| 157 | + linesStream, |
| 158 | + documentBeforeEdits, |
| 159 | + docId, |
| 160 | + undefined, |
| 161 | + undefined, |
| 162 | + new TestLogService(), |
| 163 | + () => { |
| 164 | + // Signal failure after the first edit has been yielded |
| 165 | + if (yieldCount++ > 0) { |
| 166 | + return cancellationReason; |
| 167 | + } |
| 168 | + return undefined; |
| 169 | + }, |
| 170 | + ); |
| 171 | + |
| 172 | + expect(edits).toHaveLength(1); |
| 173 | + expect(returnValue).toBe(cancellationReason); |
| 174 | + }); |
| 175 | + |
| 176 | + it('does not yield incomplete patch when fetch is cancelled mid-stream', async () => { |
| 177 | + // Stream is truncated before the last line "+one more new" is emitted, |
| 178 | + // simulating a cancellation that cuts off the response mid-patch. |
| 179 | + const truncatedPatchText = `/file.ts:0 |
| 180 | +-old |
| 181 | ++new |
| 182 | +/file.ts:5 |
| 183 | +-another old |
| 184 | ++another new`; |
| 185 | + const linesStream = AsyncIterUtils.fromArray(truncatedPatchText.split('\n')); |
| 186 | + const docId = DocumentId.create('file:///file.ts'); |
| 187 | + const documentBeforeEdits = new CurrentDocument(new StringText('old\n'), new Position(1, 1)); |
| 188 | + |
| 189 | + const cancellationReason = new NoNextEditReason.GotCancelled('afterFetchCall'); |
| 190 | + |
| 191 | + const { edits, returnValue } = await consumeHandleResponse( |
| 192 | + linesStream, |
| 193 | + documentBeforeEdits, |
| 194 | + docId, |
| 195 | + undefined, |
| 196 | + undefined, |
| 197 | + new TestLogService(), |
| 198 | + () => { |
| 199 | + // Fetch was cancelled — the full patch had "+one more new" but the |
| 200 | + // stream was resolved early so it's missing from the second patch. |
| 201 | + return cancellationReason; |
| 202 | + }, |
| 203 | + ); |
| 204 | + |
| 205 | + expect(edits).toHaveLength(0); |
| 206 | + expect(returnValue).toBe(cancellationReason); |
| 207 | + }); |
116 | 208 | }); |
0 commit comments