extension/test: add test for interactive refactoring

Since the gopls pre-release candidate is available, vscode-go
can now introduce test against the interactive refactoring.

For golang/go#76331

Change-Id: If71a3c65c4aec6eea4dd65c1c583789ca684bb33
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/778580
Reviewed-by: Madeline Kalil <mkalil@google.com>
Auto-Submit: Hongxiang Jiang <hxjiang@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/extension/test/gopls/interactive.test.ts b/extension/test/gopls/interactive.test.ts
new file mode 100644
index 0000000..686eefe
--- /dev/null
+++ b/extension/test/gopls/interactive.test.ts
@@ -0,0 +1,124 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/*---------------------------------------------------------
+ * Copyright 2026 The Go Authors. All rights reserved.
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
+ *--------------------------------------------------------*/
+
+import assert from 'assert';
+import path = require('path');
+import sinon = require('sinon');
+import vscode = require('vscode');
+import { Env } from './goplsTestEnv.utils';
+import { updateGoVarsFromConfig } from '../../src/goInstallTools';
+
+suite('Interactive Refactoring', function () {
+	this.timeout(30000);
+
+	let document: vscode.TextDocument;
+	const sandbox = sinon.createSandbox();
+	const projectDir = path.join(__dirname, '..', '..', '..');
+	const testdataDir = path.join(projectDir, 'test', 'testdata', 'interactive');
+	const env = new Env();
+
+	this.afterEach(function () {
+		env.flushTrace(this.currentTest?.state === 'failed');
+		sandbox.restore();
+	});
+
+	suiteSetup(async () => {
+		await updateGoVarsFromConfig({});
+		const uri = vscode.Uri.file(path.join(testdataDir, 'interactive.go'));
+		await env.startGopls(uri.fsPath, undefined, testdataDir);
+		document = await vscode.workspace.openTextDocument(uri);
+	});
+
+	suiteTeardown(async () => {
+		await env.teardown();
+	});
+
+	test('Add struct tags', async () => {
+		const editor = await vscode.window.showTextDocument(document);
+
+		// type Foo struct {
+		//	Foo string //@loc(editor.selection, "Foo string")
+		// }
+		editor.selection = new vscode.Selection(3, 1, 3, 11);
+
+		const codeActions = await vscode.commands.executeCommand<vscode.CodeAction[]>(
+			'vscode.executeCodeActionProvider',
+			document.uri,
+			editor.selection
+		);
+
+		const action = codeActions.find((a) => a.kind?.value === 'refactor.rewrite.addTags');
+
+		assert.ok(action, 'Add struct tags code action not found');
+		assert.ok(action.command, 'Code action has no command');
+
+		const inputBoxStub = sandbox.stub(vscode.window, 'showInputBox');
+		const quickPickStub = sandbox.stub(vscode.window, 'showQuickPick');
+
+		// First attempt fails because of invalid tags.
+		inputBoxStub.onFirstCall().resolves('json,x x,html'); // invalid, space not allowed in a tag "x x"
+		quickPickStub.onFirstCall().resolves({ value: 'camelcase', label: 'camelCase' } as any);
+
+		// Second attempt succeeds. vscode-go will ask the second question regardless.
+		inputBoxStub.onSecondCall().resolves('json,xml');
+		quickPickStub.onSecondCall().resolves({ value: 'camelcase', label: 'camelCase' } as any);
+
+		// Trigger the command. The middleware handles the interactive handshake
+		// with gopls, collects answers via our stubs, and executes the refactoring.
+		await vscode.commands.executeCommand(action.command.command, ...action.command.arguments!);
+
+		const docText = document.getText();
+		assert.match(docText, /Foo string `json:"foo" xml:"foo"`/);
+	});
+
+	test('Stub methods', async () => {
+		const editor = await vscode.window.showTextDocument(document);
+
+		// type Foo struct {
+		//	Foo string //@loc(editor.selection, "Foo string")
+		// }
+		editor.selection = new vscode.Selection(3, 1, 3, 11);
+
+		const codeActions = await vscode.commands.executeCommand<vscode.CodeAction[]>(
+			'vscode.executeCodeActionProvider',
+			document.uri,
+			editor.selection
+		);
+
+		const action = codeActions.find((a) => a.kind?.value === 'refactor.rewrite.implementInterface');
+		assert.ok(action, 'Stub methods code action not found');
+		assert.ok(action.command, 'Stub methods code action has no command');
+
+		sandbox.stub(vscode.window, 'createQuickPick').returns({
+			// In production, `onDidAccept` registers a callback that waits for a
+			// user's click.
+			// In this test, we simulate an immediate user selection by invoking the
+			// callback instantly and mocking the chosen value via `selectedItems`.
+			onDidAccept: (cb: () => void) => cb(),
+			selectedItems: [{ label: 'net.Error', value: 'net.Error' }],
+			// The following are fake properties, place holders for
+			// production code to overwrite.
+			title: '',
+			placeholder: '',
+			matchOnDescription: true,
+			onDidChangeValue: sinon.fake(),
+			onDidHide: sinon.fake(),
+			show: sinon.fake(),
+			hide: sinon.fake(),
+			dispose: sinon.fake()
+		} as any);
+
+		// Trigger the command. The middleware handles the interactive handshake
+		// with gopls, collects answers via our stubs, and executes the refactoring.
+		await vscode.commands.executeCommand(action.command.command, ...action.command.arguments!);
+
+		const docText = document.getText();
+
+		assert.match(docText, /func \(f \*Foo\) Error\(\) string/);
+		assert.match(docText, /func \(f \*Foo\) Temporary\(\) bool/);
+		assert.match(docText, /func \(f \*Foo\) Timeout\(\) bool/);
+	});
+});
diff --git a/extension/test/testdata/interactive/deps.go b/extension/test/testdata/interactive/deps.go
new file mode 100644
index 0000000..94edaf2
--- /dev/null
+++ b/extension/test/testdata/interactive/deps.go
@@ -0,0 +1,5 @@
+package interactive
+
+import "net"
+
+var _ net.Error
diff --git a/extension/test/testdata/interactive/go.mod b/extension/test/testdata/interactive/go.mod
new file mode 100644
index 0000000..d3acfb6
--- /dev/null
+++ b/extension/test/testdata/interactive/go.mod
@@ -0,0 +1,3 @@
+module github.com/microsoft/vscode-go/gofixtures/interactive
+
+go 1.18
diff --git a/extension/test/testdata/interactive/interactive.go b/extension/test/testdata/interactive/interactive.go
new file mode 100644
index 0000000..82b0383
--- /dev/null
+++ b/extension/test/testdata/interactive/interactive.go
@@ -0,0 +1,5 @@
+package interactive
+
+type Foo struct {
+	Foo string
+}