From 065dc73dc11fc03424a4ed4622dfdbb8b201268d Mon Sep 17 00:00:00 2001 From: Laxman Reddy Aileni Date: Mon, 5 Oct 2026 17:55:06 +0000 Subject: [PATCH 1/2] fix(chat): key chat history on a stable workspace identifier Send the Eclipse workspace root as awsClientCapabilities.q.workspaceFilePath in the LSP initialize request. The language server uses it to name the chat history database; without it the server falls back to hashing the set of open project folders, which changes whenever a project is opened, closed, imported or deleted. On the next restart a different history file is loaded, so previously open chat tabs are not restored (or stale tabs from an old file are restored instead). The server migrates the existing folder-based history file to the new identifier the first time it sees workspaceFilePath, so existing chat history is preserved. --- .../amazonq/lsp/AmazonQLspServerBuilder.java | 8 ++ .../eclipse/amazonq/util/WorkspaceUtils.java | 27 +++++++ .../amazonq/util/WorkspaceUtilsTest.java | 78 +++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 plugin/tst/software/aws/toolkits/eclipse/amazonq/util/WorkspaceUtilsTest.java diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspServerBuilder.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspServerBuilder.java index d43fdced6..dd6fb5dac 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspServerBuilder.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspServerBuilder.java @@ -24,6 +24,7 @@ import software.aws.toolkits.eclipse.amazonq.telemetry.metadata.ClientMetadata; import software.aws.toolkits.eclipse.amazonq.telemetry.metadata.PluginClientMetadata; import software.aws.toolkits.eclipse.amazonq.util.AbapUtil; +import software.aws.toolkits.eclipse.amazonq.util.WorkspaceUtils; public class AmazonQLspServerBuilder extends Builder { @@ -60,6 +61,13 @@ private Map getInitializationOptions(final ClientMetadata metada qOptions.put("mcp", true); qOptions.put("pinnedContextEnabled", true); qOptions.put("modelSelection", true); + // Give the server a stable identifier for this Eclipse workspace so chat history is + // keyed per workspace rather than per set-of-open-projects (which changes between + // sessions and causes previously open chat tabs not to be restored). + String workspaceFilePath = WorkspaceUtils.getWorkspaceFilePath(); + if (StringUtils.isNotBlank(workspaceFilePath)) { + qOptions.put("workspaceFilePath", workspaceFilePath); + } awsClientCapabilities.put("q", qOptions); Map window = new HashMap<>(); window.put("showSaveFileDialog", true); diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/WorkspaceUtils.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/WorkspaceUtils.java index 3332e707f..a4ca93f69 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/WorkspaceUtils.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/WorkspaceUtils.java @@ -7,6 +7,7 @@ import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; import org.eclipse.ui.IViewPart; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchWindow; @@ -19,6 +20,32 @@ public final class WorkspaceUtils { private WorkspaceUtils() { } + /** + * Returns a stable, filesystem-based identifier for the current Eclipse workspace: + * the absolute path of the workspace root (the directory that holds {@code .metadata}). + * + *

The language server keys its chat history database on this value when it is provided + * ({@code awsClientCapabilities.q.workspaceFilePath}). Without it the server falls back to + * hashing the set of open project folders, which changes whenever a project is opened, + * closed, imported or deleted, so a different history file is loaded on the next restart and + * previously open chat tabs are not restored. + * + * @return the workspace root path, or {@code null} if it cannot be determined + */ + public static String getWorkspaceFilePath() { + try { + IPath location = ResourcesPlugin.getWorkspace().getRoot().getLocation(); + if (location == null) { + return null; + } + String path = location.toOSString(); + return path.isBlank() ? null : path; + } catch (Exception e) { + Activator.getLogger().warn("Failed to determine workspace location: " + e.getMessage()); + return null; + } + } + public static void refreshAllProjects() { IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); for (IProject project : projects) { diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/WorkspaceUtilsTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/WorkspaceUtilsTest.java new file mode 100644 index 000000000..eff719d37 --- /dev/null +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/WorkspaceUtilsTest.java @@ -0,0 +1,78 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IPath; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import software.aws.toolkits.eclipse.amazonq.plugin.Activator; + +public class WorkspaceUtilsTest { + + private MockedStatic mockedResourcesPlugin; + private MockedStatic mockedActivator; + private IWorkspaceRoot mockRoot; + + @BeforeEach + public final void setUp() { + mockedResourcesPlugin = mockStatic(ResourcesPlugin.class); + mockedActivator = mockStatic(Activator.class); + mockedActivator.when(Activator::getLogger).thenReturn(mock(LoggingService.class)); + + IWorkspace mockWorkspace = mock(IWorkspace.class); + mockRoot = mock(IWorkspaceRoot.class); + when(mockWorkspace.getRoot()).thenReturn(mockRoot); + mockedResourcesPlugin.when(ResourcesPlugin::getWorkspace).thenReturn(mockWorkspace); + } + + @AfterEach + public final void tearDown() { + mockedActivator.close(); + mockedResourcesPlugin.close(); + } + + @Test + public void testGetWorkspaceFilePathReturnsWorkspaceRootLocation() { + IPath mockPath = mock(IPath.class); + when(mockPath.toOSString()).thenReturn("/home/user/workspace"); + when(mockRoot.getLocation()).thenReturn(mockPath); + + assertEquals("/home/user/workspace", WorkspaceUtils.getWorkspaceFilePath()); + } + + @Test + public void testGetWorkspaceFilePathReturnsNullWhenLocationIsNull() { + when(mockRoot.getLocation()).thenReturn(null); + + assertNull(WorkspaceUtils.getWorkspaceFilePath()); + } + + @Test + public void testGetWorkspaceFilePathReturnsNullWhenLocationIsBlank() { + IPath mockPath = mock(IPath.class); + when(mockPath.toOSString()).thenReturn(" "); + when(mockRoot.getLocation()).thenReturn(mockPath); + + assertNull(WorkspaceUtils.getWorkspaceFilePath()); + } + + @Test + public void testGetWorkspaceFilePathReturnsNullWhenWorkspaceUnavailable() { + mockedResourcesPlugin.when(ResourcesPlugin::getWorkspace).thenThrow(new IllegalStateException("not running")); + + assertNull(WorkspaceUtils.getWorkspaceFilePath()); + } +} From 4095010aa83ea0492e6efce69437ede9d1417ea6 Mon Sep 17 00:00:00 2001 From: Laxman Reddy Aileni Date: Mon, 5 Oct 2026 21:30:22 +0000 Subject: [PATCH 2/2] fix(chat): canonicalize the workspace id and cover the init capability Review feedback: - Canonicalize the workspace root path. A symlinked -data directory, a redundant path segment, or a different drive-letter case previously produced a different identifier, which defeats the point of a stable id. Falls back to the literal path if canonicalization fails. The blank check runs first, because new File("").getCanonicalPath() resolves to the process working directory. - Add AmazonQLspServerBuilderTest, driving an initialize request through wrapMessageConsumer and asserting aws.awsClientCapabilities.q. workspaceFilePath is present with the workspace root and absent when the root is unknown. That key is a plain string in a nested map, so a typo would otherwise compile and pass CI silently. - Log the caught exception instead of only its message. - Rename getWorkspaceFilePath to getWorkspaceRootPath, which describes what the method returns rather than the protocol field it feeds. - Drop the redundant isNotBlank check in the builder; the util already returns null rather than a blank string, so a null check is enough. --- .../amazonq/lsp/AmazonQLspServerBuilder.java | 6 +- .../eclipse/amazonq/util/WorkspaceUtils.java | 30 +++-- .../lsp/AmazonQLspServerBuilderTest.java | 112 ++++++++++++++++++ .../amazonq/util/WorkspaceUtilsTest.java | 74 ++++++++++-- 4 files changed, 200 insertions(+), 22 deletions(-) create mode 100644 plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspServerBuilderTest.java diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspServerBuilder.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspServerBuilder.java index dd6fb5dac..6d651f6be 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspServerBuilder.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspServerBuilder.java @@ -64,9 +64,9 @@ private Map getInitializationOptions(final ClientMetadata metada // Give the server a stable identifier for this Eclipse workspace so chat history is // keyed per workspace rather than per set-of-open-projects (which changes between // sessions and causes previously open chat tabs not to be restored). - String workspaceFilePath = WorkspaceUtils.getWorkspaceFilePath(); - if (StringUtils.isNotBlank(workspaceFilePath)) { - qOptions.put("workspaceFilePath", workspaceFilePath); + String workspaceRootPath = WorkspaceUtils.getWorkspaceRootPath(); + if (workspaceRootPath != null) { + qOptions.put("workspaceFilePath", workspaceRootPath); } awsClientCapabilities.put("q", qOptions); Map window = new HashMap<>(); diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/WorkspaceUtils.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/WorkspaceUtils.java index a4ca93f69..73dffdce8 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/WorkspaceUtils.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/WorkspaceUtils.java @@ -3,6 +3,8 @@ package software.aws.toolkits.eclipse.amazonq.util; +import java.io.IOException; + import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.ResourcesPlugin; @@ -22,26 +24,40 @@ private WorkspaceUtils() { } /** * Returns a stable, filesystem-based identifier for the current Eclipse workspace: - * the absolute path of the workspace root (the directory that holds {@code .metadata}). + * the canonical absolute path of the workspace root (the directory that holds + * {@code .metadata}). * *

The language server keys its chat history database on this value when it is provided * ({@code awsClientCapabilities.q.workspaceFilePath}). Without it the server falls back to - * hashing the set of open project folders, which changes whenever a project is opened, - * closed, imported or deleted, so a different history file is loaded on the next restart and + * hashing the open project folders, which changes whenever a project is opened, closed, + * imported or deleted, so a different history file is loaded on the next restart and * previously open chat tabs are not restored. * + *

The path is canonicalized so that a symlinked {@code -data} directory, a redundant path + * segment, or a different drive-letter case all resolve to the same identifier. If + * canonicalization fails the literal path is returned, which is still stable for the common + * case. + * * @return the workspace root path, or {@code null} if it cannot be determined */ - public static String getWorkspaceFilePath() { + public static String getWorkspaceRootPath() { try { IPath location = ResourcesPlugin.getWorkspace().getRoot().getLocation(); if (location == null) { return null; } - String path = location.toOSString(); - return path.isBlank() ? null : path; + String osPath = location.toOSString(); + if (osPath.isBlank()) { + return null; + } + try { + return location.toFile().getCanonicalPath(); + } catch (IOException e) { + Activator.getLogger().warn("Failed to canonicalize workspace location, using the literal path", e); + return osPath; + } } catch (Exception e) { - Activator.getLogger().warn("Failed to determine workspace location: " + e.getMessage()); + Activator.getLogger().warn("Failed to determine workspace location", e); return null; } } diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspServerBuilderTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspServerBuilderTest.java new file mode 100644 index 000000000..584bc8cb1 --- /dev/null +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspServerBuilderTest.java @@ -0,0 +1,112 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.lsp; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import java.util.Map; + +import org.eclipse.lsp4j.InitializeParams; +import org.eclipse.lsp4j.jsonrpc.MessageConsumer; +import org.eclipse.lsp4j.jsonrpc.messages.RequestMessage; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import software.aws.toolkits.eclipse.amazonq.telemetry.metadata.ClientMetadata; +import software.aws.toolkits.eclipse.amazonq.telemetry.metadata.PluginClientMetadata; +import software.aws.toolkits.eclipse.amazonq.util.WorkspaceUtils; + +/** + * The server keys its chat history database on + * {@code aws.awsClientCapabilities.q.workspaceFilePath}. The key is a plain string in a nested + * map, so a typo or a dropped entry would compile, pass every other test, and silently restore + * the wrong chat tabs. These tests pin the wire shape. + */ +public class AmazonQLspServerBuilderTest { + + private static final String WORKSPACE_ROOT = "/home/user/eclipse-workspace"; + + private MockedStatic mockedWorkspaceUtils; + private MockedStatic mockedClientMetadata; + + @BeforeEach + public final void setUp() { + mockedWorkspaceUtils = mockStatic(WorkspaceUtils.class); + + ClientMetadata metadata = mock(ClientMetadata.class); + when(metadata.getPluginVersion()).thenReturn("1.0.0"); + when(metadata.getClientId()).thenReturn("client-id"); + when(metadata.getIdeVersion()).thenReturn("2026-09"); + when(metadata.getIdeName()).thenReturn("Eclipse"); + mockedClientMetadata = mockStatic(PluginClientMetadata.class); + mockedClientMetadata.when(PluginClientMetadata::getInstance).thenReturn(metadata); + } + + @AfterEach + public final void tearDown() { + mockedClientMetadata.close(); + mockedWorkspaceUtils.close(); + } + + @Test + public void testSendsWorkspaceFilePathWhenWorkspaceRootIsKnown() { + mockedWorkspaceUtils.when(WorkspaceUtils::getWorkspaceRootPath).thenReturn(WORKSPACE_ROOT); + + Map qCapabilities = qCapabilitiesFromInitialize(); + + assertTrue(qCapabilities.containsKey("workspaceFilePath")); + assertEquals(WORKSPACE_ROOT, qCapabilities.get("workspaceFilePath")); + } + + @Test + public void testOmitsWorkspaceFilePathWhenWorkspaceRootIsUnknown() { + mockedWorkspaceUtils.when(WorkspaceUtils::getWorkspaceRootPath).thenReturn(null); + + Map qCapabilities = qCapabilitiesFromInitialize(); + + // Absent rather than null: the server treats any truthy value as an identifier, so a null + // entry must not be sent. + assertFalse(qCapabilities.containsKey("workspaceFilePath")); + } + + @Test + public void testKeepsOtherQCapabilitiesWhenWorkspaceRootIsUnknown() { + mockedWorkspaceUtils.when(WorkspaceUtils::getWorkspaceRootPath).thenReturn(null); + + Map qCapabilities = qCapabilitiesFromInitialize(); + + assertEquals(true, qCapabilities.get("mcp")); + assertEquals(true, qCapabilities.get("pinnedContextEnabled")); + assertEquals(true, qCapabilities.get("modelSelection")); + assertEquals(true, qCapabilities.get("developerProfiles")); + assertEquals(true, qCapabilities.get("customizationsWithMetadata")); + } + + /** + * Drives an {@code initialize} request through the builder's message consumer and returns the + * {@code aws.awsClientCapabilities.q} map the builder attached to it. + */ + @SuppressWarnings("unchecked") + private Map qCapabilitiesFromInitialize() { + InitializeParams initParams = new InitializeParams(); + RequestMessage message = new RequestMessage(); + message.setMethod("initialize"); + message.setParams(initParams); + + MessageConsumer consumer = new AmazonQLspServerBuilder().wrapMessageConsumer(m -> { }); + consumer.consume(message); + + Map initOptions = (Map) initParams.getInitializationOptions(); + Map aws = (Map) initOptions.get("aws"); + Map clientCapabilities = (Map) aws.get("awsClientCapabilities"); + return (Map) clientCapabilities.get("q"); + } +} diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/WorkspaceUtilsTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/WorkspaceUtilsTest.java index eff719d37..d112beaf7 100644 --- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/WorkspaceUtilsTest.java +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/WorkspaceUtilsTest.java @@ -5,10 +5,19 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; + import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; @@ -16,6 +25,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.mockito.MockedStatic; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; @@ -25,12 +35,14 @@ public class WorkspaceUtilsTest { private MockedStatic mockedResourcesPlugin; private MockedStatic mockedActivator; private IWorkspaceRoot mockRoot; + private LoggingService mockLogger; @BeforeEach public final void setUp() { mockedResourcesPlugin = mockStatic(ResourcesPlugin.class); mockedActivator = mockStatic(Activator.class); - mockedActivator.when(Activator::getLogger).thenReturn(mock(LoggingService.class)); + mockLogger = mock(LoggingService.class); + mockedActivator.when(Activator::getLogger).thenReturn(mockLogger); IWorkspace mockWorkspace = mock(IWorkspace.class); mockRoot = mock(IWorkspaceRoot.class); @@ -45,34 +57,72 @@ public final void tearDown() { } @Test - public void testGetWorkspaceFilePathReturnsWorkspaceRootLocation() { - IPath mockPath = mock(IPath.class); - when(mockPath.toOSString()).thenReturn("/home/user/workspace"); - when(mockRoot.getLocation()).thenReturn(mockPath); + public void testReturnsCanonicalWorkspaceRootPath(final @TempDir Path tempDir) throws IOException { + File workspaceRoot = tempDir.toFile(); + givenWorkspaceLocation(workspaceRoot.getAbsolutePath(), workspaceRoot); - assertEquals("/home/user/workspace", WorkspaceUtils.getWorkspaceFilePath()); + // Compared against getCanonicalPath() rather than a literal so the assertion holds on + // Windows and on macOS, where the temp directory is itself reached through a symlink. + assertEquals(workspaceRoot.getCanonicalPath(), WorkspaceUtils.getWorkspaceRootPath()); } @Test - public void testGetWorkspaceFilePathReturnsNullWhenLocationIsNull() { + public void testResolvesRedundantSegmentsToTheSameIdentifier(final @TempDir Path tempDir) throws IOException { + File workspaceRoot = tempDir.toFile(); + File subDirectory = new File(workspaceRoot, "projects"); + assertTrue(subDirectory.mkdir(), "failed to create fixture directory"); + File indirect = new File(subDirectory, ".."); + + givenWorkspaceLocation(indirect.getPath(), indirect); + + // The whole point of the identifier is stability: two spellings of one directory must not + // produce two different chat history files. + assertEquals(workspaceRoot.getCanonicalPath(), WorkspaceUtils.getWorkspaceRootPath()); + } + + @Test + public void testFallsBackToLiteralPathWhenCanonicalizationFails() throws IOException { + File failingFile = mock(File.class); + when(failingFile.getCanonicalPath()).thenThrow(new IOException("cannot resolve")); + + givenWorkspaceLocation("/home/user/workspace", failingFile); + + assertEquals("/home/user/workspace", WorkspaceUtils.getWorkspaceRootPath()); + verify(mockLogger).warn( + eq("Failed to canonicalize workspace location, using the literal path"), + any(IOException.class)); + } + + @Test + public void testReturnsNullWhenLocationIsNull() { when(mockRoot.getLocation()).thenReturn(null); - assertNull(WorkspaceUtils.getWorkspaceFilePath()); + assertNull(WorkspaceUtils.getWorkspaceRootPath()); } @Test - public void testGetWorkspaceFilePathReturnsNullWhenLocationIsBlank() { + public void testReturnsNullWhenLocationIsBlankWithoutTouchingTheFilesystem() { IPath mockPath = mock(IPath.class); when(mockPath.toOSString()).thenReturn(" "); when(mockRoot.getLocation()).thenReturn(mockPath); - assertNull(WorkspaceUtils.getWorkspaceFilePath()); + assertNull(WorkspaceUtils.getWorkspaceRootPath()); + // A blank path must short-circuit: new File("").getCanonicalPath() resolves to the process + // working directory, which would be a wrong and unstable identifier. + verify(mockPath, never()).toFile(); } @Test - public void testGetWorkspaceFilePathReturnsNullWhenWorkspaceUnavailable() { + public void testReturnsNullWhenWorkspaceUnavailable() { mockedResourcesPlugin.when(ResourcesPlugin::getWorkspace).thenThrow(new IllegalStateException("not running")); - assertNull(WorkspaceUtils.getWorkspaceFilePath()); + assertNull(WorkspaceUtils.getWorkspaceRootPath()); + } + + private void givenWorkspaceLocation(final String osPath, final File file) { + IPath mockPath = mock(IPath.class); + when(mockPath.toOSString()).thenReturn(osPath); + when(mockPath.toFile()).thenReturn(file); + when(mockRoot.getLocation()).thenReturn(mockPath); } }