Path: blob/main/extensions/copilot/test/pipeline/replayRecording.ts
13389 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { IRecordingInformation, ObservableWorkspaceRecordingReplayer } from '../../src/extension/inlineEdits/common/observableWorkspaceRecordingReplayer';6import { DocumentId } from '../../src/platform/inlineEdits/common/dataTypes/documentId';7import { IObservableDocument, MutableObservableWorkspace } from '../../src/platform/inlineEdits/common/observableWorkspace';8import { coalesce } from '../../src/util/vs/base/common/arrays';9import { Processor } from './alternativeAction/processor';10import { IInputRow } from './parseInput';1112/**13* Result of processing a single input row: replayed workspace + oracle edit.14*/15export interface IProcessedRow {16readonly originalRowIndex: number;17readonly row: IInputRow;18readonly replayer: ObservableWorkspaceRecordingReplayer;19readonly workspace: MutableObservableWorkspace;20readonly activeDocId: DocumentId;21readonly activeDocument: IObservableDocument;22readonly activeFilePath: string;23/** What the user actually typed next (from post-request recording). */24readonly nextUserEdit: {25readonly edit: readonly (readonly [start: number, endEx: number, text: string])[];26readonly relativePath: string;27readonly originalOpIdx: number;28};29readonly recordingInfo: IRecordingInformation;30}3132/**33* Parse a suggestedEdit string like `[978, 1021) -> "foo"` into `[start, endEx, text]`.34* The text portion is JSON-encoded (from `JSON.stringify`), so we parse it back.35*/36export function parseSuggestedEdit(suggestedEditStr: string): [start: number, endEx: number, text: string] | null {37const separator = ' -> ';38const delimiterIdx = suggestedEditStr.indexOf(separator);39if (delimiterIdx === -1) {40return null;41}42const stringifiedRange = suggestedEditStr.substring(0, delimiterIdx);43const quotedText = suggestedEditStr.substring(delimiterIdx + separator.length);44const match = stringifiedRange.match(/^\[(\d+), (\d+)\)$/);45if (!match || !quotedText) {46return null;47}48const start = parseInt(match[1], 10);49const endEx = parseInt(match[2], 10);50try {51const text = JSON.parse(quotedText) as string;52return [start, endEx, text];53} catch {54return null;55}56}5758function formatError(e: unknown): string {59if (e instanceof Error) {60if (e.message === 'An unexpected bug occurred.' && e.stack) {61const frames = e.stack.split('\n').slice(1, 4).map(f => f.trim()).join(' <- ');62return `${e.message} Stack: ${frames}`;63}64return e.message;65}66return String(e);67}6869/**70* Process a single input row: split recording at request time, replay71* the pre-request portion and extract the oracle edit.72*/73export function processRow(row: IInputRow): IProcessedRow | { error: string } {74try {75return _processRow(row);76} catch (e: unknown) {77return { error: `Unexpected error: ${formatError(e)}` };78}79}8081function _processRow(row: IInputRow): IProcessedRow | { error: string } {82const proposedEdits = coalesce([parseSuggestedEdit(row.postProcessingOutcome.suggestedEdit)]);83const isAccepted = row.suggestionStatus === 'accepted';8485const scoring = Processor.createScoringForAlternativeAction(86row.alternativeAction,87proposedEdits,88isAccepted,89);9091if (!scoring) {92const entryCount = row.alternativeAction?.recording?.entries?.length ?? 0;93return { error: `Processor.createScoringForAlternativeAction returned undefined (${entryCount} entries, lang: ${row.activeDocumentLanguageId})` };94}9596const recording = scoring.scoringContext.recording;9798const recordingInfo: IRecordingInformation = {99log: recording.log,100nextUserEdit: {101relativePath: recording.nextUserEdit.relativePath,102edit: recording.nextUserEdit.edit,103},104};105106const replayer = new ObservableWorkspaceRecordingReplayer(recordingInfo);107let lastDocId: DocumentId;108try {109const result = replayer.replay();110lastDocId = result.lastDocId;111} catch (e) {112replayer.dispose();113return { error: `Replay failed (${recording.log.length} entries, file: ${recording.nextUserEdit?.relativePath ?? 'unknown'}): ${formatError(e)}` };114}115116const workspace = replayer.workspace;117const activeDocument = workspace.getDocument(lastDocId);118if (!activeDocument) {119replayer.dispose();120return { error: `Active document not found after replay: ${lastDocId}` };121}122123// Prefer scoring edit URI, fall back to oracle path124const activeFilePath = scoring.edits[0]?.documentUri ?? recording.nextUserEdit?.relativePath ?? 'unknown';125126return {127originalRowIndex: row.originalRowIndex,128row,129replayer,130workspace,131activeDocId: lastDocId,132activeDocument,133activeFilePath,134nextUserEdit: recording.nextUserEdit,135recordingInfo,136};137}138139/**140* Process all input rows.141* Each returned `IProcessedRow` holds a live replayer that must be disposed by the caller.142*/143export function processAllRows(rows: readonly IInputRow[]): {144processed: IProcessedRow[];145errors: { rowIndex: number; error: string }[];146} {147const processed: IProcessedRow[] = [];148const errors: { rowIndex: number; error: string }[] = [];149150for (let i = 0; i < rows.length; i++) {151const result = processRow(rows[i]);152if ('error' in result) {153errors.push({ rowIndex: i, error: result.error });154} else {155processed.push(result);156}157}158159return { processed, errors };160}161162163