Add prompt for updating GitHub databases

This commit is contained in:
Koen Vlaswinkel
2023-11-20 12:07:39 +01:00
parent 4f51445609
commit a0295d62f8
6 changed files with 858 additions and 15 deletions

View File

@@ -441,6 +441,19 @@
"Never download a GitHub databases when a workspace is opened."
],
"description": "Ask to download a GitHub database when a workspace is opened."
},
"codeQL.githubDatabase.update": {
"type": "string",
"default": "ask",
"enum": [
"ask",
"never"
],
"enumDescriptions": [
"Ask to download an updated GitHub database when a new version is available.",
"Never download an updated GitHub database when a new version is available."
],
"description": "Ask to download an updated GitHub database when a new version is available."
}
}
},

View File

@@ -788,13 +788,23 @@ const GITHUB_DATABASE_DOWNLOAD = new Setting(
const GitHubDatabaseDownloadValues = ["ask", "never"] as const;
type GitHubDatabaseDownload = (typeof GitHubDatabaseDownloadValues)[number];
const GITHUB_DATABASE_UPDATE = new Setting("update", GITHUB_DATABASE_SETTING);
const GitHubDatabaseUpdateValues = ["ask", "never"] as const;
type GitHubDatabaseUpdate = (typeof GitHubDatabaseUpdateValues)[number];
export interface GitHubDatabaseConfig {
enable: boolean;
download: GitHubDatabaseDownload;
update: GitHubDatabaseUpdate;
setDownload(
value: GitHubDatabaseDownload,
target?: ConfigurationTarget,
): Promise<void>;
setUpdate(
value: GitHubDatabaseUpdate,
target?: ConfigurationTarget,
): Promise<void>;
}
export class GitHubDatabaseConfigListener
@@ -817,10 +827,22 @@ export class GitHubDatabaseConfigListener
return GitHubDatabaseDownloadValues.includes(value) ? value : "ask";
}
public get update(): GitHubDatabaseUpdate {
const value = GITHUB_DATABASE_UPDATE.getValue<GitHubDatabaseUpdate>();
return GitHubDatabaseUpdateValues.includes(value) ? value : "ask";
}
public async setDownload(
value: GitHubDatabaseDownload,
target: ConfigurationTarget = ConfigurationTarget.Workspace,
): Promise<void> {
await GITHUB_DATABASE_DOWNLOAD.updateValue(value, target);
}
public async setUpdate(
value: GitHubDatabaseUpdate,
target: ConfigurationTarget = ConfigurationTarget.Workspace,
): Promise<void> {
await GITHUB_DATABASE_UPDATE.updateValue(value, target);
}
}

View File

@@ -110,7 +110,7 @@ export async function downloadDatabaseFromGitHub(
*
* @param languages The languages to join. These should be language identifiers, such as `csharp`.
*/
function joinLanguages(languages: string[]): string {
export function joinLanguages(languages: string[]): string {
const languageDisplayNames = languages
.map((language) => getLanguageDisplayName(language))
.sort();
@@ -130,8 +130,17 @@ function joinLanguages(languages: string[]): string {
return result;
}
async function promptForDatabases(
type PromptForDatabasesOptions = {
title?: string;
placeHolder?: string;
};
export async function promptForDatabases(
databases: CodeqlDatabase[],
{
title = "Select databases to download",
placeHolder = "Databases found in this repository",
}: PromptForDatabasesOptions = {},
): Promise<CodeqlDatabase[]> {
if (databases.length === 1) {
return databases;
@@ -152,8 +161,8 @@ async function promptForDatabases(
.sort((a, b) => a.label.localeCompare(b.label));
const selectedItems = await window.showQuickPick(items, {
title: "Select databases to download",
placeHolder: "Databases found in this repository",
title,
placeHolder,
ignoreFocusOut: true,
canPickMany: true,
});

View File

@@ -3,7 +3,7 @@ import { DisposableObject } from "../common/disposable-object";
import { App } from "../common/app";
import { findGitHubRepositoryForWorkspace } from "./github-repository-finder";
import { redactableError } from "../common/errors";
import { asError, getErrorMessage } from "../common/helpers-pure";
import { asError, assertNever, getErrorMessage } from "../common/helpers-pure";
import {
askForGitHubDatabaseDownload,
downloadDatabaseFromGitHub,
@@ -12,6 +12,11 @@ import { GitHubDatabaseConfig, GitHubDatabaseConfigListener } from "../config";
import { DatabaseManager } from "./local-databases";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { listDatabases, ListDatabasesResult } from "./github-database-api";
import {
askForGitHubDatabaseUpdate,
downloadDatabaseUpdateFromGitHub,
isNewerDatabaseAvailable,
} from "./github-database-updates";
export class GithubDatabaseModule extends DisposableObject {
private readonly config: GitHubDatabaseConfig;
@@ -79,16 +84,6 @@ export class GithubDatabaseModule extends DisposableObject {
const githubRepository = githubRepositoryResult.value;
const hasExistingDatabase = this.databaseManager.databaseItems.some(
(db) =>
db.origin?.type === "github" &&
db.origin.repository ===
`${githubRepository.owner}/${githubRepository.name}`,
);
if (hasExistingDatabase) {
return;
}
let result: ListDatabasesResult | undefined;
try {
result = await listDatabases(
@@ -130,6 +125,48 @@ export class GithubDatabaseModule extends DisposableObject {
return;
}
const updateStatus = isNewerDatabaseAvailable(
databases,
githubRepository.owner,
githubRepository.name,
this.databaseManager,
);
if (updateStatus.type === "upToDate") {
return;
}
if (updateStatus.type === "updateAvailable") {
if (this.config.update === "never") {
return;
}
if (
!(await askForGitHubDatabaseUpdate(
updateStatus.databaseUpdates,
this.config,
))
) {
return;
}
await downloadDatabaseUpdateFromGitHub(
octokit,
githubRepository.owner,
githubRepository.name,
updateStatus.databaseUpdates,
this.databaseManager,
this.databaseStoragePath,
this.cliServer,
this.app.commands,
);
return;
}
if (updateStatus.type !== "noDatabase") {
assertNever(updateStatus);
}
// If the user already had an access token, first ask if they even want to download the DB.
if (!promptedForCredentials) {
if (!(await askForGitHubDatabaseDownload(databases, this.config))) {

View File

@@ -0,0 +1,205 @@
import { CodeqlDatabase } from "./github-database-api";
import { DatabaseItem, DatabaseManager } from "./local-databases";
import { Octokit } from "@octokit/rest";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { AppCommandManager } from "../common/commands";
import { getLanguageDisplayName } from "../common/query-language";
import { showNeverAskAgainDialog } from "../common/vscode/dialog";
import { downloadGitHubDatabaseFromUrl } from "./database-fetcher";
import { withProgress } from "../common/vscode/progress";
import { window } from "vscode";
import { GitHubDatabaseConfig } from "../config";
import { joinLanguages, promptForDatabases } from "./github-database-download";
export type DatabaseUpdate = {
database: CodeqlDatabase;
databaseItem: DatabaseItem;
};
type DatabaseUpdateStatusUpdateAvailable = {
type: "updateAvailable";
databaseUpdates: DatabaseUpdate[];
};
type DatabaseUpdateStatusUpToDate = {
type: "upToDate";
};
type DatabaseUpdateStatusNoDatabase = {
type: "noDatabase";
};
type DatabaseUpdateStatus =
| DatabaseUpdateStatusUpdateAvailable
| DatabaseUpdateStatusUpToDate
| DatabaseUpdateStatusNoDatabase;
export function isNewerDatabaseAvailable(
databases: CodeqlDatabase[],
owner: string,
name: string,
databaseManager: DatabaseManager,
): DatabaseUpdateStatus {
// Sorted by date added ascending
const existingDatabasesForRepository = databaseManager.databaseItems
.filter(
(db) =>
db.origin?.type === "github" &&
db.origin.repository === `${owner}/${name}`,
)
.sort((a, b) => (a.dateAdded ?? 0) - (b.dateAdded ?? 0));
if (existingDatabasesForRepository.length === 0) {
return {
type: "noDatabase",
};
}
// Sort order is guaranteed by the sort call above. The newest database is the last one.
const newestExistingDatabasesByLanguage = new Map<string, DatabaseItem>();
for (const existingDatabase of existingDatabasesForRepository) {
newestExistingDatabasesByLanguage.set(
existingDatabase.language,
existingDatabase,
);
}
const databaseUpdates = Array.from(newestExistingDatabasesByLanguage.values())
.map((newestExistingDatabase): DatabaseUpdate | null => {
const origin = newestExistingDatabase.origin;
if (origin?.type !== "github") {
return null;
}
const matchingDatabase = databases.find(
(db) => db.language === newestExistingDatabase.language,
);
if (!matchingDatabase) {
return null;
}
if (matchingDatabase.commit_oid === origin.commitOid) {
return null;
}
return {
database: matchingDatabase,
databaseItem: newestExistingDatabase,
};
})
.filter((update): update is DatabaseUpdate => update !== null)
.sort((a, b) => a.database.language.localeCompare(b.database.language));
if (databaseUpdates.length === 0) {
return {
type: "upToDate",
};
}
return {
type: "updateAvailable",
databaseUpdates,
};
}
export async function askForGitHubDatabaseUpdate(
updates: DatabaseUpdate[],
config: GitHubDatabaseConfig,
): Promise<boolean> {
const languages = updates.map((update) => update.database.language);
const message =
updates.length === 1
? `There is a newer ${getLanguageDisplayName(
languages[0],
)} CodeQL database available for this repository. Download the database update from GitHub?`
: `There are newer ${joinLanguages(
languages,
)} CodeQL databases available for this repository. Download the database updates from GitHub?`;
const answer = await showNeverAskAgainDialog(
message,
false,
"Download",
"Not now",
"Never",
);
if (answer === "Not now" || answer === undefined) {
return false;
}
if (answer === "Never") {
await config.setUpdate("never");
return false;
}
return true;
}
export async function downloadDatabaseUpdateFromGitHub(
octokit: Octokit,
owner: string,
repo: string,
updates: DatabaseUpdate[],
databaseManager: DatabaseManager,
storagePath: string,
cliServer: CodeQLCliServer,
commandManager: AppCommandManager,
): Promise<void> {
const selectedDatabases = await promptForDatabases(
updates.map((update) => update.database),
{
title: "Select databases to update",
},
);
if (selectedDatabases.length === 0) {
return;
}
await Promise.all(
selectedDatabases.map((database) => {
const update = updates.find((update) => update.database === database);
if (!update) {
return;
}
return withProgress(
async (progress) => {
const newDatabase = await downloadGitHubDatabaseFromUrl(
database.url,
database.id,
database.created_at,
database.commit_oid ?? null,
owner,
repo,
octokit,
progress,
databaseManager,
storagePath,
cliServer,
databaseManager.currentDatabaseItem === update.databaseItem,
update.databaseItem.hasSourceArchiveInExplorer(),
);
if (newDatabase === undefined) {
return;
}
await databaseManager.removeDatabaseItem(update.databaseItem);
await commandManager.execute("codeQLDatabases.focus");
void window.showInformationMessage(
`Updated ${getLanguageDisplayName(
database.language,
)} database from GitHub.`,
);
},
{
title: `Updating ${getLanguageDisplayName(
database.language,
)} database from GitHub`,
},
);
}),
);
}

View File

@@ -0,0 +1,557 @@
import { faker } from "@faker-js/faker";
import { Octokit } from "@octokit/rest";
import { QuickPickItem, window } from "vscode";
import {
mockDatabaseItem,
mockedObject,
mockedQuickPickItem,
} from "../../utils/mocking.helpers";
import { CodeqlDatabase } from "../../../../src/databases/github-database-api";
import { DatabaseManager } from "../../../../src/databases/local-databases";
import { GitHubDatabaseConfig } from "../../../../src/config";
import { CodeQLCliServer } from "../../../../src/codeql-cli/cli";
import { createMockCommandManager } from "../../../__mocks__/commandsMock";
import * as databaseFetcher from "../../../../src/databases/database-fetcher";
import * as dialog from "../../../../src/common/vscode/dialog";
import {
DatabaseUpdate,
askForGitHubDatabaseUpdate,
downloadDatabaseUpdateFromGitHub,
isNewerDatabaseAvailable,
} from "../../../../src/databases/github-database-updates";
describe("isNewerDatabaseAvailable", () => {
const owner = "github";
const repo = "codeql";
let databases: CodeqlDatabase[];
let databaseManager: DatabaseManager;
describe("when there are updates", () => {
beforeEach(() => {
databases = [
mockedObject<CodeqlDatabase>({
language: "java",
commit_oid: "58e7476df3e464a0c9742b14cd4ca274b0993ebb",
}),
mockedObject<CodeqlDatabase>({
language: "swift",
commit_oid: "b81c25c0b73dd3c242068e8ab38bef25563a7c2d",
}),
mockedObject<CodeqlDatabase>({
language: "javascript",
commit_oid: "6e93915ff37ff8bcfc552d48f118895d60d0e7cd",
}),
mockedObject<CodeqlDatabase>({
language: "ql",
commit_oid: "9448fbfb88cdefe4298cc2e234a5a3c98958cae8",
}),
];
databaseManager = mockedObject<DatabaseManager>({
databaseItems: [
mockDatabaseItem({
dateAdded: 1600477451789,
language: "java",
origin: {
type: "github",
repository: "github/codeql",
commitOid: "4487d1da9665231d1a076c60a78523f6275ad70f",
},
}),
mockDatabaseItem({
dateAdded: 50,
language: "swift",
origin: {
type: "github",
repository: "github/codeql",
commitOid: "2b020927d3c6eb407223a1baa3d6ce3597a3f88d",
},
}),
mockDatabaseItem({
dateAdded: 1700477451789,
language: "java",
origin: {
type: "github",
repository: "github/codeql",
commitOid: "17663af4e84a3a010fcb3f09cc06049797dfb22a",
},
}),
mockDatabaseItem({
dateAdded: faker.date.past().getTime(),
language: "golang",
origin: {
type: "github",
repository: "github/codeql",
},
}),
mockDatabaseItem({
dateAdded: faker.date.past().getTime(),
language: "ql",
origin: {
type: "github",
repository: "github/codeql",
},
}),
mockDatabaseItem({
language: "javascript",
origin: {
type: "github",
repository: "github/vscode-codeql",
commitOid: "fb360f9c09ac8c5edb2f18be5de4e80ea4c430d0",
},
}),
],
});
});
it("returns the correct status", async () => {
expect(
isNewerDatabaseAvailable(databases, owner, repo, databaseManager),
).toEqual({
type: "updateAvailable",
databaseUpdates: [
{
database: databases[0],
databaseItem: databaseManager.databaseItems[2],
},
{
database: databases[3],
databaseItem: databaseManager.databaseItems[4],
},
{
database: databases[1],
databaseItem: databaseManager.databaseItems[1],
},
],
});
});
});
describe("when there are no updates", () => {
beforeEach(() => {
databases = [
mockedObject<CodeqlDatabase>({
language: "java",
commit_oid: "17663af4e84a3a010fcb3f09cc06049797dfb22a",
}),
];
databaseManager = mockedObject<DatabaseManager>({
databaseItems: [
mockDatabaseItem({
dateAdded: 1700477451789,
language: "java",
origin: {
type: "github",
repository: "github/codeql",
commitOid: "17663af4e84a3a010fcb3f09cc06049797dfb22a",
},
}),
mockDatabaseItem({
dateAdded: 1600477451789,
language: "java",
origin: {
type: "github",
repository: "github/codeql",
commitOid: "4487d1da9665231d1a076c60a78523f6275ad70f",
},
}),
],
});
});
it("returns the correct status", async () => {
expect(
isNewerDatabaseAvailable(databases, owner, repo, databaseManager),
).toEqual({
type: "upToDate",
});
});
});
describe("when there are no matching database items", () => {
beforeEach(() => {
databases = [
mockedObject<CodeqlDatabase>({
language: "java",
commit_oid: "17663af4e84a3a010fcb3f09cc06049797dfb22a",
}),
];
databaseManager = mockedObject<DatabaseManager>({
databaseItems: [
mockDatabaseItem({
language: "javascript",
origin: {
type: "github",
repository: "github/vscode-codeql",
commitOid: "fb360f9c09ac8c5edb2f18be5de4e80ea4c430d0",
},
}),
],
});
});
it("returns the correct status", async () => {
expect(
isNewerDatabaseAvailable(databases, owner, repo, databaseManager),
).toEqual({
type: "noDatabase",
});
});
});
});
describe("askForGitHubDatabaseUpdate", () => {
const setUpdate = jest.fn();
let config: GitHubDatabaseConfig;
const updates: DatabaseUpdate[] = [
{
database: mockedObject<CodeqlDatabase>({
id: faker.number.int(),
created_at: faker.date.past().toISOString(),
commit_oid: faker.git.commitSha(),
language: "swift",
size: 27389673,
url: faker.internet.url({
protocol: "https",
}),
}),
databaseItem: mockDatabaseItem(),
},
];
let showNeverAskAgainDialogSpy: jest.SpiedFunction<
typeof dialog.showNeverAskAgainDialog
>;
beforeEach(() => {
config = mockedObject<GitHubDatabaseConfig>({
setUpdate,
});
showNeverAskAgainDialogSpy = jest.spyOn(dialog, "showNeverAskAgainDialog");
});
describe("when answering download to prompt", () => {
beforeEach(() => {
showNeverAskAgainDialogSpy.mockResolvedValue("Download");
});
it("returns false", async () => {
expect(await askForGitHubDatabaseUpdate(updates, config)).toEqual(true);
expect(setUpdate).not.toHaveBeenCalled();
});
});
describe("when answering not now to prompt", () => {
beforeEach(() => {
showNeverAskAgainDialogSpy.mockResolvedValue("Not now");
});
it("returns false", async () => {
expect(await askForGitHubDatabaseUpdate(updates, config)).toEqual(false);
expect(setUpdate).not.toHaveBeenCalled();
});
});
describe("when cancelling prompt", () => {
beforeEach(() => {
showNeverAskAgainDialogSpy.mockResolvedValue(undefined);
});
it("returns false", async () => {
expect(await askForGitHubDatabaseUpdate(updates, config)).toEqual(false);
expect(setUpdate).not.toHaveBeenCalled();
});
});
describe("when answering never to prompt", () => {
beforeEach(() => {
showNeverAskAgainDialogSpy.mockResolvedValue("Never");
});
it("returns false", async () => {
expect(await askForGitHubDatabaseUpdate(updates, config)).toEqual(false);
});
it("sets the config to never", async () => {
await askForGitHubDatabaseUpdate(updates, config);
expect(setUpdate).toHaveBeenCalledWith("never");
});
});
});
describe("downloadDatabaseUpdateFromGitHub", () => {
let octokit: Octokit;
const owner = "github";
const repo = "codeql";
let databaseManager: DatabaseManager;
const storagePath = "/a/b/c/d";
let cliServer: CodeQLCliServer;
const commandManager = createMockCommandManager();
let updates: DatabaseUpdate[] = [
{
database: mockedObject<CodeqlDatabase>({
id: faker.number.int(),
created_at: faker.date.past().toISOString(),
commit_oid: faker.git.commitSha(),
language: "swift",
size: 27389673,
url: faker.internet.url({
protocol: "https",
}),
}),
databaseItem: mockDatabaseItem({
hasSourceArchiveInExplorer: () => false,
}),
},
];
let showQuickPickSpy: jest.SpiedFunction<typeof window.showQuickPick>;
let downloadGitHubDatabaseFromUrlSpy: jest.SpiedFunction<
typeof databaseFetcher.downloadGitHubDatabaseFromUrl
>;
beforeEach(() => {
octokit = mockedObject<Octokit>({});
databaseManager = mockedObject<DatabaseManager>({
currentDatabaseItem: mockDatabaseItem(),
});
cliServer = mockedObject<CodeQLCliServer>({});
showQuickPickSpy = jest.spyOn(window, "showQuickPick").mockResolvedValue(
mockedQuickPickItem([
mockedObject<QuickPickItem & { database: CodeqlDatabase }>({
database: updates[0].database,
}),
]),
);
downloadGitHubDatabaseFromUrlSpy = jest
.spyOn(databaseFetcher, "downloadGitHubDatabaseFromUrl")
.mockResolvedValue(undefined);
});
it("downloads the database", async () => {
await downloadDatabaseUpdateFromGitHub(
octokit,
owner,
repo,
updates,
databaseManager,
storagePath,
cliServer,
commandManager,
);
expect(downloadGitHubDatabaseFromUrlSpy).toHaveBeenCalledTimes(1);
expect(downloadGitHubDatabaseFromUrlSpy).toHaveBeenCalledWith(
updates[0].database.url,
updates[0].database.id,
updates[0].database.created_at,
updates[0].database.commit_oid,
owner,
repo,
octokit,
expect.anything(),
databaseManager,
storagePath,
cliServer,
false,
false,
);
expect(showQuickPickSpy).not.toHaveBeenCalled();
});
describe("when there are multiple languages", () => {
beforeEach(() => {
updates = [
{
database: mockedObject<CodeqlDatabase>({
id: faker.number.int(),
created_at: faker.date.past().toISOString(),
commit_oid: faker.git.commitSha(),
language: "swift",
size: 27389673,
url: faker.internet.url({
protocol: "https",
}),
}),
databaseItem: mockDatabaseItem({
hasSourceArchiveInExplorer: () => false,
}),
},
{
database: mockedObject<CodeqlDatabase>({
id: faker.number.int(),
created_at: faker.date.past().toISOString(),
commit_oid: null,
language: "go",
size: 2930572385,
url: faker.internet.url({
protocol: "https",
}),
}),
databaseItem: mockDatabaseItem({
hasSourceArchiveInExplorer: () => true,
}),
},
];
databaseManager = mockedObject<DatabaseManager>({
currentDatabaseItem: updates[1].databaseItem,
});
});
it("downloads a single selected language", async () => {
showQuickPickSpy.mockResolvedValue(
mockedQuickPickItem([
mockedObject<QuickPickItem & { database: CodeqlDatabase }>({
database: updates[1].database,
}),
]),
);
await downloadDatabaseUpdateFromGitHub(
octokit,
owner,
repo,
updates,
databaseManager,
storagePath,
cliServer,
commandManager,
);
expect(downloadGitHubDatabaseFromUrlSpy).toHaveBeenCalledTimes(1);
expect(downloadGitHubDatabaseFromUrlSpy).toHaveBeenCalledWith(
updates[1].database.url,
updates[1].database.id,
updates[1].database.created_at,
updates[1].database.commit_oid,
owner,
repo,
octokit,
expect.anything(),
databaseManager,
storagePath,
cliServer,
true,
true,
);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
expect.objectContaining({
label: "Go",
description: "2794.8 MB",
database: updates[1].database,
}),
expect.objectContaining({
label: "Swift",
description: "26.1 MB",
database: updates[0].database,
}),
],
expect.anything(),
);
});
it("downloads multiple selected languages", async () => {
showQuickPickSpy.mockResolvedValue(
mockedQuickPickItem([
mockedObject<QuickPickItem & { database: CodeqlDatabase }>({
database: updates[0].database,
}),
mockedObject<QuickPickItem & { database: CodeqlDatabase }>({
database: updates[1].database,
}),
]),
);
await downloadDatabaseUpdateFromGitHub(
octokit,
owner,
repo,
updates,
databaseManager,
storagePath,
cliServer,
commandManager,
);
expect(downloadGitHubDatabaseFromUrlSpy).toHaveBeenCalledTimes(2);
expect(downloadGitHubDatabaseFromUrlSpy).toHaveBeenCalledWith(
updates[0].database.url,
updates[0].database.id,
updates[0].database.created_at,
updates[0].database.commit_oid,
owner,
repo,
octokit,
expect.anything(),
databaseManager,
storagePath,
cliServer,
false,
false,
);
expect(downloadGitHubDatabaseFromUrlSpy).toHaveBeenCalledWith(
updates[1].database.url,
updates[1].database.id,
updates[1].database.created_at,
updates[1].database.commit_oid,
owner,
repo,
octokit,
expect.anything(),
databaseManager,
storagePath,
cliServer,
true,
true,
);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
expect.objectContaining({
label: "Go",
description: "2794.8 MB",
database: updates[1].database,
}),
expect.objectContaining({
label: "Swift",
description: "26.1 MB",
database: updates[0].database,
}),
],
expect.anything(),
);
});
describe("when not selecting language", () => {
beforeEach(() => {
showQuickPickSpy.mockResolvedValue(undefined);
});
it("does not download the database", async () => {
await downloadDatabaseUpdateFromGitHub(
octokit,
owner,
repo,
updates,
databaseManager,
storagePath,
cliServer,
commandManager,
);
expect(downloadGitHubDatabaseFromUrlSpy).not.toHaveBeenCalled();
});
});
});
});