Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package com.microsoft.copilot.eclipse.core.chat.service;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.nio.file.Path;
import java.util.Set;
import java.util.concurrent.CompletableFuture;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection;
import com.microsoft.copilot.eclipse.core.lsp.protocol.CustomizationFileInfo;

class CustomizationFileServiceTests {

@TempDir
Path tempDir;

@Test
void testRefreshAllAsync_ignoresResultOfSupersededRefresh() {
CopilotLanguageServerConnection lsConnection = mock(CopilotLanguageServerConnection.class);
CompletableFuture<CustomizationFileInfo[]> olderRefresh = new CompletableFuture<>();
CompletableFuture<CustomizationFileInfo[]> newerRefresh = new CompletableFuture<>();
when(lsConnection.listCustomInstructions(any())).thenReturn(olderRefresh, newerRefresh);
when(lsConnection.listCustomSkills(any())).thenReturn(emptyResult());
when(lsConnection.listCustomPrompts(any())).thenReturn(emptyResult());
when(lsConnection.listCustomAgents(any())).thenReturn(emptyResult());
Path staleFile = tempDir.resolve("stale.instructions.md");
Path currentFile = tempDir.resolve("current.instructions.md");

CustomizationFileService service = new CustomizationFileService(lsConnection);
try {
service.refreshAllAsync();
verify(lsConnection, timeout(5000).times(1)).listCustomInstructions(any());
service.refreshAllAsync();
verify(lsConnection, timeout(5000).times(2)).listCustomInstructions(any());

newerRefresh.complete(new CustomizationFileInfo[] { toFileInfo(currentFile) });
olderRefresh.complete(new CustomizationFileInfo[] { toFileInfo(staleFile) });

assertEquals(Set.of(currentFile.toAbsolutePath().normalize()), service.getCustomizationFiles());
} finally {
service.dispose();
}
}

private static CompletableFuture<CustomizationFileInfo[]> emptyResult() {
return CompletableFuture.completedFuture(new CustomizationFileInfo[0]);
}

private static CustomizationFileInfo toFileInfo(Path file) {
return new CustomizationFileInfo(null, file.getFileName().toString(), file.toUri().toString(), "local");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package com.microsoft.copilot.eclipse.core.lsp.protocol;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.List;

import com.google.gson.Gson;
import org.junit.jupiter.api.Test;

class ConversationTemplateTests {

private final Gson gson = new Gson();

@Test
void testCommandName_promptFile_returnsName() {
String json = """
{
"id": "file:///c%3A/repo/.github/prompts/review.prompt.md",
"name": "review",
"description": "",
"scopes": ["chat-panel", "agent-panel"],
"source": "prompt"
}
""";

ConversationTemplate template = gson.fromJson(json, ConversationTemplate.class);

assertEquals("review", template.commandName());
}

@Test
void testCommandName_promptFileWithoutName_returnsId() {
ConversationTemplate template = new ConversationTemplate("file:///repo/review.prompt.md", null, null, null,
List.of(), TemplateSource.PROMPT);

assertEquals("file:///repo/review.prompt.md", template.commandName());
}

@Test
void testCommandName_skill_returnsId() {
ConversationTemplate template = new ConversationTemplate("skill:marker-skill", "marker-skill", null, null,
List.of(), TemplateSource.SKILL);

assertEquals("skill:marker-skill", template.commandName());
}

@Test
void testCommandName_builtin_returnsId() {
ConversationTemplate template = new ConversationTemplate("explain", null, null, null, List.of(),
TemplateSource.BUILTIN);

assertEquals("explain", template.commandName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,41 @@

package com.microsoft.copilot.eclipse.core.utils;

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.when;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.lsp4j.WorkspaceFolder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import com.microsoft.copilot.eclipse.core.Constants;

class WorkspaceUtilsTests {

private static final String UI_PREFERENCE_NODE = "com.microsoft.copilot.eclipse.ui";

@TempDir
Path tempDir;

@AfterEach
void tearDown() {
uiPreferences().remove(Constants.CUSTOM_INSTRUCTIONS_PARENT_REPO_ENABLED);
}

@Test
void testIsGitRepository_withGitFolder() throws IOException {
// Create a temporary .git directory
Expand Down Expand Up @@ -97,4 +112,139 @@ void testIsGitRepository_withNullLocation() {
assertFalse(WorkspaceUtils.isGitRepository(project));
}

@Test
void testFindRepositoryRoot_withGitFolderInFolderItself() throws IOException {
Path repo = Files.createDirectories(tempDir.resolve("repo/.git")).getParent();

assertEquals(Optional.of(repo), WorkspaceUtils.findRepositoryRoot(repo));
}

@Test
void testFindRepositoryRoot_withGitFolderInAncestor() throws IOException {
Path repo = Files.createDirectories(tempDir.resolve("repo/.git")).getParent();
Path module = Files.createDirectories(repo.resolve("modules/app"));

assertEquals(Optional.of(repo), WorkspaceUtils.findRepositoryRoot(module));
}

@Test
void testFindRepositoryRoot_withGitFile() throws IOException {
// git worktrees and submodules use a .git file instead of a folder
Path worktree = Files.createDirectories(tempDir.resolve("worktree"));
Files.createFile(worktree.resolve(".git"));
Path module = Files.createDirectories(worktree.resolve("app"));

assertEquals(Optional.of(worktree), WorkspaceUtils.findRepositoryRoot(module));
}

@Test
void testFindRepositoryRoot_outsideRepository() throws IOException {
Path folder = Files.createDirectories(tempDir.resolve("no-repo/app"));

// only check below the temp dir, the temp dir itself might be located in a git repository
assertTrue(WorkspaceUtils.findRepositoryRoot(folder).filter(root -> root.startsWith(tempDir)).isEmpty());
}

@Test
void testWithParentRepositoryFolders_addsAncestorsUpToRepositoryRoot() throws IOException {
Path repo = Files.createDirectories(tempDir.resolve("repo/.git")).getParent();
Path modules = Files.createDirectories(repo.resolve("modules"));
Path app = Files.createDirectories(modules.resolve("app"));

List<WorkspaceFolder> result = WorkspaceUtils.withParentRepositoryFolders(List.of(toWorkspaceFolder(app)));

assertEquals(List.of(app, modules, repo), toPaths(result));
assertEquals(List.of("app", "modules", "repo"), result.stream().map(WorkspaceFolder::getName).toList());
}

@Test
void testWithParentRepositoryFolders_keepsRepositoryRootUnchanged() throws IOException {
Path repo = Files.createDirectories(tempDir.resolve("repo/.git")).getParent();
List<WorkspaceFolder> folders = List.of(toWorkspaceFolder(repo));

assertEquals(folders, WorkspaceUtils.withParentRepositoryFolders(folders));
}

@Test
void testWithParentRepositoryFolders_avoidsDuplicates() throws IOException {
Path repo = Files.createDirectories(tempDir.resolve("repo/.git")).getParent();
Path moduleA = Files.createDirectories(repo.resolve("a"));
Path moduleB = Files.createDirectories(repo.resolve("b"));

List<WorkspaceFolder> result = WorkspaceUtils.withParentRepositoryFolders(
List.of(toWorkspaceFolder(moduleA), toWorkspaceFolder(moduleB), toWorkspaceFolder(repo)));

assertEquals(List.of(moduleA, moduleB, repo), toPaths(result));
}

@Test
void testWithParentRepositoryFolders_stopsAtSubmoduleRoot() throws IOException {
Path repo = Files.createDirectories(tempDir.resolve("repo/.git")).getParent();
Path submodule = Files.createDirectories(repo.resolve("submodule"));
Files.createFile(submodule.resolve(".git"));
Path app = Files.createDirectories(submodule.resolve("app"));

List<WorkspaceFolder> result = WorkspaceUtils.withParentRepositoryFolders(List.of(toWorkspaceFolder(app)));

assertEquals(List.of(app, submodule), toPaths(result));
}

@Test
void testWithParentRepositoryFolders_keepsNonFileFolders() {
List<WorkspaceFolder> folders = List.of(new WorkspaceFolder("jdt://contents/rt.jar", "rt.jar"));

assertEquals(folders, WorkspaceUtils.withParentRepositoryFolders(folders));
}

@Test
void testListParentRepositoryFolders_returnsOnlyAdditionalAncestors() throws IOException {
Path repo = Files.createDirectories(tempDir.resolve("repo/.git")).getParent();
Path modules = Files.createDirectories(repo.resolve("modules"));
Path app = Files.createDirectories(modules.resolve("app"));

List<WorkspaceFolder> result = WorkspaceUtils.listParentRepositoryFolders(
List.of(toWorkspaceFolder(app), toWorkspaceFolder(modules)));

assertEquals(List.of(repo), toPaths(result));
}

@Test
void testIsParentRepositoryEnabled_trueByDefault() {
assertTrue(WorkspaceUtils.isParentRepositoryEnabled());
}

@Test
void testWithParentRepositoryFoldersIfEnabled_addsAncestorsWhenEnabled() throws IOException {
Path repo = Files.createDirectories(tempDir.resolve("repo/.git")).getParent();
Path app = Files.createDirectories(repo.resolve("app"));
uiPreferences().putBoolean(Constants.CUSTOM_INSTRUCTIONS_PARENT_REPO_ENABLED, true);

List<WorkspaceFolder> result = WorkspaceUtils.withParentRepositoryFoldersIfEnabled(
List.of(toWorkspaceFolder(app)));

assertEquals(List.of(app, repo), toPaths(result));
}

@Test
void testWithParentRepositoryFoldersIfEnabled_keepsFoldersWhenDisabled() throws IOException {
Path repo = Files.createDirectories(tempDir.resolve("repo/.git")).getParent();
Path app = Files.createDirectories(repo.resolve("app"));
uiPreferences().putBoolean(Constants.CUSTOM_INSTRUCTIONS_PARENT_REPO_ENABLED, false);
List<WorkspaceFolder> folders = List.of(toWorkspaceFolder(app));

assertEquals(folders, WorkspaceUtils.withParentRepositoryFoldersIfEnabled(folders));
}

private static IEclipsePreferences uiPreferences() {
return InstanceScope.INSTANCE.getNode(UI_PREFERENCE_NODE);
}

private static WorkspaceFolder toWorkspaceFolder(Path path) {
return new WorkspaceFolder(path.toUri().toASCIIString(), path.getFileName().toString());
}

private static List<Path> toPaths(List<WorkspaceFolder> folders) {
return folders.stream().map(folder -> FileUtils.getLocalFilePath(folder.getUri())).toList();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ private Constants() {
public static final String CUSTOM_INSTRUCTIONS_CHAT_LOAD_SCOPE = "customInstructionsChatLoadScope";
public static final String CUSTOM_INSTRUCTIONS_CHAT_LOAD_SCOPE_ALL = "allProjects";
public static final String CUSTOM_INSTRUCTIONS_CHAT_LOAD_SCOPE_REFERENCED = "referencedProjects";
public static final String CUSTOM_INSTRUCTIONS_PARENT_REPO_ENABLED = "customInstructionsParentRepoEnabled";
public static final String GITHUB_COPILOT_URL = "http://github.com";
@Deprecated
public static final String QUICK_START_VERSION = "quickStartVersion";
Expand Down
Loading