extension/src: use full path mode when running go test

The fullpath mode is added in go 1.21 and the vscode-go is supporting
the last three minor Go versions (1.23.0+ as of 2025-09).

If the user is beyond the support, the extension will send a
notification asking user to upgrade.

In case user does not want to upgrade, set "-test.fullpath=false"
to setting "go.testFlags" will fall back to the original behavior.

Fix golang/vscode-go#3853

Change-Id: I06b723b715651ea0bf98ae62797917af7b1c74b2
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/704776
Reviewed-by: Madeline Kalil <mkalil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 47b8226..761ea99 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,19 @@
 
 ## Unreleased
 
+### Important
+
+* To ensure the extension remains fully compatible and stable, the required
+minimum Go version remains Go 1.23. A new notification will now be sent to help
+users running older versions upgrade to Go 1.23+.
+
+### Fixes
+
+* Corrected an issue where clicking on a failing test in the Test Explorer would
+open a non-existent Go file (golang/vscode-go#3853). This occurred when the test
+entry point (e.g., .../foo_test.go) was in a different directory than the file
+where the failure actually occurred (e.g., .../bar/bar_test.go).
+
 ## v0.51.0 (prerelease)
 
 Date: 2025-09-04
diff --git a/extension/src/goEnvironmentStatus.ts b/extension/src/goEnvironmentStatus.ts
index babec5d..31d5d8d 100644
--- a/extension/src/goEnvironmentStatus.ts
+++ b/extension/src/goEnvironmentStatus.ts
@@ -13,6 +13,7 @@
 import os = require('os');
 import path = require('path');
 import { promisify } from 'util';
+import semverCoerce from 'semver/functions/coerce';
 import { getGoConfig, extensionInfo } from './config';
 import { toolInstallationEnvironment } from './goEnv';
 import { addGoStatus, goEnvStatusbarItem, outputChannel, removeGoStatus } from './goStatus';
@@ -549,7 +550,12 @@
 }
 
 const STATUS_BAR_ITEM_NAME = 'Go Update Notification';
-const dismissedGoVersionUpdatesKey = 'dismissedGoVersionUpdates';
+
+/**
+ * Key for the global state that tracks Go versions for which the user has
+ * explicitly dismissed the upgrade notification.
+ */
+const DISMISSED_GO_VERSION_KEY = 'dismissedGoVersionUpdates';
 
 export async function offerToInstallLatestGoVersion(ctx: Pick<vscode.ExtensionContext, 'subscriptions'>) {
 	if (extensionInfo.isInCloudIDE) {
@@ -564,83 +570,96 @@
 		return;
 	}
 
-	let options = await getLatestGoVersions();
+	let latestVersions = await getLatestGoVersions();
 
-	// Filter out Go versions the user has already dismissed.
-	let dismissedOptions: GoEnvironmentOption[];
-	dismissedOptions = await getFromGlobalState(dismissedGoVersionUpdatesKey);
-	if (dismissedOptions) {
-		options = options.filter((version) => !dismissedOptions.find((x) => x.label === version.label));
+	const currentVersion = await getGoVersion();
+
+	// The official support for vscode-go is last three minor versions of Go.
+	// Try to start with last four minor versions.
+	let minimumVersion = semverCoerce(latestVersions[0].label);
+	if (minimumVersion) {
+		minimumVersion.minor = minimumVersion.minor - 3;
+		minimumVersion.patch = 0;
 	}
 
-	// Compare to current go version.
-	const currentVersion = await getGoVersion();
+	const download = {
+		title: 'Download',
+		async command() {
+			await vscode.env.openExternal(vscode.Uri.parse('https://go.dev/dl/'));
+		}
+	};
+
+	// Popup if the Go version is beyond support.
+	if (minimumVersion && currentVersion.lt(minimumVersion.format())) {
+		let text = `The minimum supported Go version is ${minimumVersion.format()}. Please update your Go to ensure the extension functions correctly. You are currently using ${formatGoVersion(
+			currentVersion
+		)}.`;
+		vscode.window.showInformationMessage(text, download).then((selection) => {
+			selection?.command();
+		});
+	}
+
+	// Filter out Go versions the user has already dismissed.
+	const dismissedVersions: GoEnvironmentOption[] = await getFromGlobalState(DISMISSED_GO_VERSION_KEY);
+	if (dismissedVersions) {
+		latestVersions = latestVersions.filter((version) => !dismissedVersions.find((x) => x.label === version.label));
+	}
+
+	// Filter out Go versions below the current go versions.
 	if (currentVersion) {
-		options = options.filter((version) => currentVersion.lt(version.label));
+		latestVersions = latestVersions.filter((version) => currentVersion.lt(version.label));
 	}
 
 	// Notify user that there is a newer version of Go available.
-	if (options.length > 0) {
-		const versionsText = options.map((x) => x.label).join(', ');
-		const statusBarItem = addGoStatus(STATUS_BAR_ITEM_NAME);
-		statusBarItem.name = STATUS_BAR_ITEM_NAME;
-		statusBarItem.text = 'New Go version is available';
-		statusBarItem.detail = versionsText;
-		statusBarItem.command = {
-			title: 'Upgrade',
-			command: 'go.promptforgoinstall',
-			arguments: [options],
-			tooltip: 'Upgrade or silence notification'
-		};
-		// TODO: Error level is more visible. Consider to make it configurable?
-		statusBarItem.severity = vscode.LanguageStatusSeverity.Warning;
-
+	if (latestVersions.length > 0) {
 		ctx.subscriptions.push(
 			vscode.commands.registerCommand('go.promptforgoinstall', () => {
-				const download = {
-					title: 'Download',
-					async command() {
-						await vscode.env.openExternal(vscode.Uri.parse('https://go.dev/dl/'));
-					}
-				};
-
 				const neverAgain = {
 					title: "Don't Show Again",
 					async command() {
 						// Mark these versions as seen.
-						dismissedOptions = await getFromGlobalState(dismissedGoVersionUpdatesKey);
-						if (!dismissedOptions) {
-							dismissedOptions = [];
+						let dismissedVersions: GoEnvironmentOption[] = await getFromGlobalState(
+							DISMISSED_GO_VERSION_KEY
+						);
+						if (!dismissedVersions) {
+							dismissedVersions = [];
 						}
-						options.forEach((version) => {
-							dismissedOptions.push(version);
+						latestVersions.forEach((version) => {
+							dismissedVersions.push(version);
 						});
-						await updateGlobalState(dismissedGoVersionUpdatesKey, dismissedOptions);
+						await updateGlobalState(DISMISSED_GO_VERSION_KEY, dismissedVersions);
 					}
 				};
 
-				let versionsText: string;
-				if (options.length > 1) {
-					versionsText = `${options
+				let text: string;
+				if (latestVersions.length > 1) {
+					text = `${latestVersions
 						.map((x) => x.label)
 						.reduce((prev, next) => {
 							return prev + ' and ' + next;
 						})} are available`;
 				} else {
-					versionsText = `${options[0].label} is available`;
+					text = `${latestVersions[0].label} is available.`;
 				}
-
-				vscode.window
-					.showInformationMessage(
-						`${versionsText}. You are currently using ${formatGoVersion(currentVersion)}.`,
-						download,
-						neverAgain
-					)
-					.then((selection) => {
-						selection?.command();
-						removeGoStatus(STATUS_BAR_ITEM_NAME);
-					});
+				text = text + ` You are currently using ${formatGoVersion(currentVersion)}.`;
+				vscode.window.showInformationMessage(text, download, neverAgain).then((selection) => {
+					selection?.command();
+					removeGoStatus(STATUS_BAR_ITEM_NAME);
+				});
 			})
 		);
+
+		const statusBarItem = addGoStatus(STATUS_BAR_ITEM_NAME);
+		statusBarItem.name = STATUS_BAR_ITEM_NAME;
+		statusBarItem.text = 'New Go version is available';
+		statusBarItem.detail = latestVersions.map((x) => x.label).join(', ');
+		statusBarItem.command = {
+			title: 'Upgrade',
+			command: 'go.promptforgoinstall',
+			arguments: [latestVersions],
+			tooltip: 'Upgrade or silence notification'
+		};
+		// TODO: Error level is more visible. Consider to make it configurable?
+		statusBarItem.severity = vscode.LanguageStatusSeverity.Warning;
 	}
 }
diff --git a/extension/src/goMain.ts b/extension/src/goMain.ts
index 52915d0..537d68e 100644
--- a/extension/src/goMain.ts
+++ b/extension/src/goMain.ts
@@ -45,7 +45,7 @@
 import { GO111MODULE, goModInit } from './goModules';
 import { playgroundCommand } from './goPlayground';
 import { GoRunTestCodeLensProvider } from './goRunTestCodelens';
-import { disposeGoStatusBar, expandGoStatusBar, outputChannel, updateGoStatusBar } from './goStatus';
+import { disposeGoStatusBar, expandGoStatusBar, updateGoStatusBar } from './goStatus';
 
 import { vetCode } from './goVet';
 import {
@@ -75,7 +75,6 @@
 import { GoTaskProvider } from './goTaskProvider';
 import { setTelemetryEnvVars, activationLatency, telemetryReporter } from './goTelemetry';
 import { experiments } from './experimental';
-import { allToolsInformation } from './goToolsInformation';
 
 const goCtx: GoExtensionContext = {};
 
diff --git a/extension/src/testUtils.ts b/extension/src/testUtils.ts
index 4070f6e..72a185f 100644
--- a/extension/src/testUtils.ts
+++ b/extension/src/testUtils.ts
@@ -544,7 +544,8 @@
 	tmpCoverPath?: string; // coverage file path if coverage info is necessary.
 	addJSONFlag: boolean | undefined; // true if we add extra -json flag for stream processing.
 } {
-	const args: Array<string> = ['test'];
+	// By default, enable full path mode to address golang/vscode-go#3853.
+	const args: Array<string> = ['test', '-test.fullpath=true'];
 	// user-specified flags
 	const argsFlagIdx = testconfig.flags?.indexOf('-args') ?? -1;
 	const userFlags = argsFlagIdx < 0 ? testconfig.flags : testconfig.flags.slice(0, argsFlagIdx);
diff --git a/extension/test/integration/test.test.ts b/extension/test/integration/test.test.ts
index 500b176..34c1c19 100644
--- a/extension/test/integration/test.test.ts
+++ b/extension/test/integration/test.test.ts
@@ -42,57 +42,57 @@
 
 	test('default config', () => {
 		runTest({
-			expectedArgs: 'test -timeout 30s ./...',
-			expectedOutArgs: 'test -timeout 30s ./...'
+			expectedArgs: 'test -test.fullpath=true -timeout 30s ./...',
+			expectedOutArgs: 'test -test.fullpath=true -timeout 30s ./...'
 		});
 	});
 	test('user flag [-v] enables -json flag', () => {
 		runTest({
-			expectedArgs: 'test -timeout 30s -json ./... -v',
-			expectedOutArgs: 'test -timeout 30s ./... -v',
+			expectedArgs: 'test -test.fullpath=true -timeout 30s -json ./... -v',
+			expectedOutArgs: 'test -test.fullpath=true -timeout 30s ./... -v',
 			flags: ['-v']
 		});
 	});
 	test('user flag [-json -v] prevents -json flag addition', () => {
 		runTest({
-			expectedArgs: 'test -timeout 30s ./... -json -v',
-			expectedOutArgs: 'test -timeout 30s ./... -json -v',
+			expectedArgs: 'test -test.fullpath=true -timeout 30s ./... -json -v',
+			expectedOutArgs: 'test -test.fullpath=true -timeout 30s ./... -json -v',
 			flags: ['-json', '-v']
 		});
 	});
 	test('user flag [-args] does not crash', () => {
 		runTest({
-			expectedArgs: 'test -timeout 30s ./... -args',
-			expectedOutArgs: 'test -timeout 30s ./... -args',
+			expectedArgs: 'test -test.fullpath=true -timeout 30s ./... -args',
+			expectedOutArgs: 'test -test.fullpath=true -timeout 30s ./... -args',
 			flags: ['-args']
 		});
 	});
 	test('user flag [-args -v] does not enable -json flag', () => {
 		runTest({
-			expectedArgs: 'test -timeout 30s ./... -args -v',
-			expectedOutArgs: 'test -timeout 30s ./... -args -v',
+			expectedArgs: 'test -test.fullpath=true -timeout 30s ./... -args -v',
+			expectedOutArgs: 'test -test.fullpath=true -timeout 30s ./... -args -v',
 			flags: ['-args', '-v']
 		});
 	});
 	test('specifying functions adds -run flags', () => {
 		runTest({
-			expectedArgs: 'test -timeout 30s -run ^(TestA|TestB)$ ./...',
-			expectedOutArgs: 'test -timeout 30s -run ^(TestA|TestB)$ ./...',
+			expectedArgs: 'test -test.fullpath=true -timeout 30s -run ^(TestA|TestB)$ ./...',
+			expectedOutArgs: 'test -test.fullpath=true -timeout 30s -run ^(TestA|TestB)$ ./...',
 			functions: ['TestA', 'TestB']
 		});
 	});
 	test('functions & benchmark adds -bench flags and skips timeout', () => {
 		runTest({
-			expectedArgs: 'test -benchmem -run=^$ -bench ^(TestA|TestB)$ ./...',
-			expectedOutArgs: 'test -benchmem -run=^$ -bench ^(TestA|TestB)$ ./...',
+			expectedArgs: 'test -test.fullpath=true -benchmem -run=^$ -bench ^(TestA|TestB)$ ./...',
+			expectedOutArgs: 'test -test.fullpath=true -benchmem -run=^$ -bench ^(TestA|TestB)$ ./...',
 			functions: ['TestA', 'TestB'],
 			isBenchmark: true
 		});
 	});
 	test('user -run flag is ignored when functions are provided', () => {
 		runTest({
-			expectedArgs: 'test -timeout 30s -run ^(TestA|TestB)$ ./...',
-			expectedOutArgs: 'test -timeout 30s -run ^(TestA|TestB)$ ./...',
+			expectedArgs: 'test -test.fullpath=true -timeout 30s -run ^(TestA|TestB)$ ./...',
+			expectedOutArgs: 'test -test.fullpath=true -timeout 30s -run ^(TestA|TestB)$ ./...',
 			functions: ['TestA', 'TestB'],
 			flags: ['-run', 'TestC']
 		});
@@ -100,9 +100,9 @@
 	test('use -testify.m for methods', () => {
 		runTest({
 			expectedArgs:
-				'test -timeout 30s -run ^TestExampleTestSuite$ -testify.m ^(TestExample|TestAnotherExample)$ ./...',
+				'test -test.fullpath=true -timeout 30s -run ^TestExampleTestSuite$ -testify.m ^(TestExample|TestAnotherExample)$ ./...',
 			expectedOutArgs:
-				'test -timeout 30s -run ^TestExampleTestSuite$ -testify.m ^(TestExample|TestAnotherExample)$ ./...',
+				'test -test.fullpath=true -timeout 30s -run ^TestExampleTestSuite$ -testify.m ^(TestExample|TestAnotherExample)$ ./...',
 			functions: [
 				'(*ExampleTestSuite).TestExample',
 				'(*ExampleTestSuite).TestAnotherExample',