Path: blob/main/src/vs/workbench/contrib/chat/test/browser/planReviewFeedbackService.test.ts
13406 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 assert from 'assert';6import { IPlanReviewFeedbackService, PlanReviewFeedbackService } from '../../browser/planReviewFeedback/planReviewFeedbackService.js';7import { DisposableStore } from '../../../../../base/common/lifecycle.js';8import { URI } from '../../../../../base/common/uri.js';9import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';1011function feedbackSummary(items: readonly { line: number; column: number }[]): string[] {12return items.map(f => `${f.line}:${f.column}`);13}1415suite('PlanReviewFeedbackService - Ordering', () => {1617const store = new DisposableStore();18let service: IPlanReviewFeedbackService;19let planUri: URI;2021setup(() => {22service = store.add(new PlanReviewFeedbackService());23planUri = URI.parse('file:///plan.md');24store.add(service.registerPlanReview(planUri, () => { }));25});2627teardown(() => {28store.clear();29});3031ensureNoDisposablesAreLeakedInTestSuite();3233test('items sorted by line number', () => {34service.addFeedback(planUri, 20, 1, 'line 20');35service.addFeedback(planUri, 5, 1, 'line 5');36service.addFeedback(planUri, 10, 1, 'line 10');3738assert.deepStrictEqual(feedbackSummary(service.getFeedback(planUri)), [39'5:1',40'10:1',41'20:1',42]);43});4445test('items sorted by line then column', () => {46service.addFeedback(planUri, 10, 20, 'col 20');47service.addFeedback(planUri, 10, 5, 'col 5');48service.addFeedback(planUri, 10, 10, 'col 10');4950assert.deepStrictEqual(feedbackSummary(service.getFeedback(planUri)), [51'10:5',52'10:10',53'10:20',54]);55});5657test('removing feedback preserves ordering', () => {58const id1 = service.addFeedback(planUri, 30, 1, 'line 30');59service.addFeedback(planUri, 10, 1, 'line 10');60service.addFeedback(planUri, 20, 1, 'line 20');6162assert.deepStrictEqual(feedbackSummary(service.getFeedback(planUri)), [63'10:1',64'20:1',65'30:1',66]);6768service.removeFeedback(planUri, id1);69assert.deepStrictEqual(feedbackSummary(service.getFeedback(planUri)), [70'10:1',71'20:1',72]);73});7475test('same line number items are stable', () => {76const id1 = service.addFeedback(planUri, 10, 1, 'first');77const id2 = service.addFeedback(planUri, 10, 1, 'second');7879const items = service.getFeedback(planUri);80assert.strictEqual(items[0].id, id1);81assert.strictEqual(items[1].id, id2);82});8384test('clear removes all items', () => {85service.addFeedback(planUri, 1, 1, 'a');86service.addFeedback(planUri, 2, 1, 'b');87service.addFeedback(planUri, 3, 1, 'c');8889assert.strictEqual(service.getFeedback(planUri).length, 3);90service.clearFeedback(planUri);91assert.strictEqual(service.getFeedback(planUri).length, 0);92});9394test('update feedback changes text', () => {95const id = service.addFeedback(planUri, 10, 1, 'original');96service.updateFeedback(planUri, id, 'updated');9798const items = service.getFeedback(planUri);99assert.strictEqual(items.length, 1);100assert.strictEqual(items[0].text, 'updated');101assert.strictEqual(items[0].line, 10);102});103});104105suite('PlanReviewFeedbackService - Navigation', () => {106107const store = new DisposableStore();108let service: IPlanReviewFeedbackService;109let planUri: URI;110111setup(() => {112service = store.add(new PlanReviewFeedbackService());113planUri = URI.parse('file:///plan.md');114store.add(service.registerPlanReview(planUri, () => { }));115});116117teardown(() => {118store.clear();119});120121ensureNoDisposablesAreLeakedInTestSuite();122123test('navigation follows sorted order', () => {124service.addFeedback(planUri, 20, 1, 'line 20');125service.addFeedback(planUri, 5, 1, 'line 5');126service.addFeedback(planUri, 10, 1, 'line 10');127128// Expected order: 5, 10, 20129const first = service.getNextFeedback(planUri, true)!;130assert.strictEqual(first.line, 5);131132const second = service.getNextFeedback(planUri, true)!;133assert.strictEqual(second.line, 10);134135const third = service.getNextFeedback(planUri, true)!;136assert.strictEqual(third.line, 20);137138// Wraps around139const fourth = service.getNextFeedback(planUri, true)!;140assert.strictEqual(fourth.line, 5);141});142143test('navigation backwards', () => {144service.addFeedback(planUri, 5, 1, 'line 5');145service.addFeedback(planUri, 10, 1, 'line 10');146service.addFeedback(planUri, 20, 1, 'line 20');147148// First backward nav goes to last item149const first = service.getNextFeedback(planUri, false)!;150assert.strictEqual(first.line, 20);151152const second = service.getNextFeedback(planUri, false)!;153assert.strictEqual(second.line, 10);154155const third = service.getNextFeedback(planUri, false)!;156assert.strictEqual(third.line, 5);157158// Wraps around159const fourth = service.getNextFeedback(planUri, false)!;160assert.strictEqual(fourth.line, 20);161});162163test('navigation bearings reflect sorted position', () => {164service.addFeedback(planUri, 20, 1, 'line 20');165service.addFeedback(planUri, 5, 1, 'line 5');166service.addFeedback(planUri, 10, 1, 'line 10');167168// Before navigation, no anchor169let bearing = service.getNavigationBearing(planUri);170assert.strictEqual(bearing.activeIdx, -1);171assert.strictEqual(bearing.totalCount, 3);172173// Navigate to first (5)174service.getNextFeedback(planUri, true);175bearing = service.getNavigationBearing(planUri);176assert.strictEqual(bearing.activeIdx, 0);177178// Navigate to second (10)179service.getNextFeedback(planUri, true);180bearing = service.getNavigationBearing(planUri);181assert.strictEqual(bearing.activeIdx, 1);182183// Navigate to third (20)184service.getNextFeedback(planUri, true);185bearing = service.getNavigationBearing(planUri);186assert.strictEqual(bearing.activeIdx, 2);187});188189test('navigation returns undefined for empty feedback', () => {190const result = service.getNextFeedback(planUri, true);191assert.strictEqual(result, undefined);192});193194test('setNavigationAnchor updates the anchor', () => {195const id = service.addFeedback(planUri, 10, 1, 'line 10');196service.addFeedback(planUri, 20, 1, 'line 20');197198service.setNavigationAnchor(planUri, id);199const bearing = service.getNavigationBearing(planUri);200assert.strictEqual(bearing.activeIdx, 0);201});202});203204suite('PlanReviewFeedbackService - Registration', () => {205206const store = new DisposableStore();207let service: IPlanReviewFeedbackService;208209setup(() => {210service = store.add(new PlanReviewFeedbackService());211});212213teardown(() => {214store.clear();215});216217ensureNoDisposablesAreLeakedInTestSuite();218219test('isActivePlanReview returns false before registration', () => {220const planUri = URI.parse('file:///plan.md');221assert.strictEqual(service.isActivePlanReview(planUri), false);222});223224test('isActivePlanReview returns true after registration', () => {225const planUri = URI.parse('file:///plan.md');226store.add(service.registerPlanReview(planUri, () => { }));227assert.strictEqual(service.isActivePlanReview(planUri), true);228});229230test('isActivePlanReview returns false after dispose', () => {231const planUri = URI.parse('file:///plan.md');232const registration = service.registerPlanReview(planUri, () => { });233assert.strictEqual(service.isActivePlanReview(planUri), true);234registration.dispose();235assert.strictEqual(service.isActivePlanReview(planUri), false);236});237238test('feedback cannot be added to unregistered plan', () => {239const planUri = URI.parse('file:///plan.md');240const id = service.addFeedback(planUri, 1, 1, 'text');241assert.strictEqual(id, '');242assert.strictEqual(service.getFeedback(planUri).length, 0);243});244245test('dispose clears feedback items', () => {246const planUri = URI.parse('file:///plan.md');247const registration = service.registerPlanReview(planUri, () => { });248service.addFeedback(planUri, 1, 1, 'text');249assert.strictEqual(service.getFeedback(planUri).length, 1);250registration.dispose();251assert.strictEqual(service.getFeedback(planUri).length, 0);252});253254test('onDidChangeRegistrations fires on register and dispose', () => {255const planUri = URI.parse('file:///plan.md');256let fireCount = 0;257store.add(service.onDidChangeRegistrations(() => fireCount++));258259const registration = service.registerPlanReview(planUri, () => { });260assert.strictEqual(fireCount, 1);261262registration.dispose();263assert.strictEqual(fireCount, 2);264});265266test('onDidChangeFeedback fires on add and remove', () => {267const planUri = URI.parse('file:///plan.md');268store.add(service.registerPlanReview(planUri, () => { }));269270let fireCount = 0;271store.add(service.onDidChangeFeedback(() => fireCount++));272273const id = service.addFeedback(planUri, 1, 1, 'text');274assert.strictEqual(fireCount, 1);275276service.removeFeedback(planUri, id);277assert.strictEqual(fireCount, 2);278});279});280281suite('PlanReviewFeedbackService - Submit', () => {282283const store = new DisposableStore();284let service: IPlanReviewFeedbackService;285286setup(() => {287service = store.add(new PlanReviewFeedbackService());288});289290teardown(() => {291store.clear();292});293294ensureNoDisposablesAreLeakedInTestSuite();295296test('submitAllFeedback calls onSubmit with formatted feedback', () => {297const planUri = URI.parse('file:///plan.md');298let submittedResult: { rejected: boolean; feedback?: string } | undefined;299store.add(service.registerPlanReview(planUri, (result) => { submittedResult = result; }));300301service.addFeedback(planUri, 1, 1, 'fix this');302service.addFeedback(planUri, 45, 45, 'change that');303304service.submitAllFeedback(planUri);305306assert.ok(submittedResult);307assert.strictEqual(submittedResult!.rejected, false);308assert.strictEqual(submittedResult!.feedback, [309'Here\'s the feedback:',310'Line 1: fix this',311'Line 45: Column 45: change that',312].join('\n'));313});314315test('submitAllFeedback does nothing when no items', () => {316const planUri = URI.parse('file:///plan.md');317let called = false;318store.add(service.registerPlanReview(planUri, () => { called = true; }));319320service.submitAllFeedback(planUri);321assert.strictEqual(called, false);322});323324test('feedback at column 1 omits column', () => {325const planUri = URI.parse('file:///plan.md');326let submittedResult: { feedback?: string } | undefined;327store.add(service.registerPlanReview(planUri, (result) => { submittedResult = result; }));328329service.addFeedback(planUri, 10, 1, 'at start');330331service.submitAllFeedback(planUri);332333assert.ok(submittedResult);334assert.strictEqual(submittedResult!.feedback, [335'Here\'s the feedback:',336'Line 10: at start',337].join('\n'));338});339340test('feedback at column > 1 includes column', () => {341const planUri = URI.parse('file:///plan.md');342let submittedResult: { feedback?: string } | undefined;343store.add(service.registerPlanReview(planUri, (result) => { submittedResult = result; }));344345service.addFeedback(planUri, 10, 15, 'mid line');346347service.submitAllFeedback(planUri);348349assert.ok(submittedResult);350assert.strictEqual(submittedResult!.feedback, [351'Here\'s the feedback:',352'Line 10: Column 15: mid line',353].join('\n'));354});355});356357358