From e7f0e8d4b79b5479f1633100984f8de709b21c9e Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Thu, 9 Jul 2026 11:48:11 +0300 Subject: [PATCH] refactor: move memento-backed state out of SecretsManager Deployment access timestamps, surfaced banner keys, and the legacy url key now live in MementoManager, keeping SecretsManager secrets-only. SecretsManager takes a MementoManager and still orchestrates per-deployment cleanup so both stores stay in sync, and AnnouncementManager depends on MementoManager directly. The surfaced banner read-modify-write moved into MementoManager.addSurfacedBanners. Keys and formats are unchanged, so no data migration. Closes #1023 --- src/announcements/manager.ts | 34 +++------ src/core/container.ts | 2 +- src/core/mementoManager.ts | 75 ++++++++++++++++++- src/core/secretsManager.ts | 57 +++----------- src/extension.ts | 2 +- test/unit/announcements/manager.test.ts | 25 +++---- test/unit/api/authInterceptor.test.ts | 7 +- test/unit/core/mementoManager.test.ts | 44 +++++++++++ test/unit/core/secretsManager.test.ts | 40 +++------- .../unit/deployment/deploymentManager.test.ts | 6 +- test/unit/login/loginCoordinator.test.ts | 8 +- test/unit/oauth/testUtils.ts | 7 +- test/unit/uri/uriHandler.test.ts | 8 +- 13 files changed, 187 insertions(+), 128 deletions(-) diff --git a/src/announcements/manager.ts b/src/announcements/manager.ts index 08047b7e3..f780aee81 100644 --- a/src/announcements/manager.ts +++ b/src/announcements/manager.ts @@ -16,7 +16,7 @@ import { import { AnnouncementsPreview } from "./preview"; import type { CoderApi } from "../api/coderApi"; -import type { SecretsManager } from "../core/secretsManager"; +import type { MementoManager } from "../core/mementoManager"; import type { SessionState } from "../deployment/sessionStore"; import type { Logger } from "../logging/logger"; @@ -43,7 +43,7 @@ export class AnnouncementManager implements vscode.Disposable { public constructor( private readonly client: Pick, private readonly sessionState: SessionState, - private readonly secretsManager: SecretsManager, + private readonly mementoManager: MementoManager, private readonly logger: Logger, ) { this.statusBarItem.command = "coder.viewAnnouncements"; @@ -149,7 +149,7 @@ export class AnnouncementManager implements vscode.Disposable { this.banners = banners; const surfacedKeys = new Set( - this.secretsManager.getSurfacedBanners(session.deployment.safeHostname), + this.mementoManager.getSurfacedBanners(session.deployment.safeHostname), ); const newBanners = banners.filter( (banner) => !surfacedKeys.has(banner.key), @@ -168,36 +168,20 @@ export class AnnouncementManager implements vscode.Disposable { return; } this.showPopup(newBanners); - await this.markSurfaced(banners, surfacedKeys); + await this.markSurfaced(banners); } /** Marks banners surfaced and clears the attention color; logs storage failures. */ - private async markSurfaced( - banners: readonly Announcement[], - knownSurfacedKeys?: ReadonlySet, - ): Promise { + private async markSurfaced(banners: readonly Announcement[]): Promise { const session = this.sessionState.current; if (session.kind !== "signedIn") { return; } try { - const surfacedKeys = - knownSurfacedKeys ?? - new Set( - this.secretsManager.getSurfacedBanners( - session.deployment.safeHostname, - ), - ); - const merged = new Set([ - ...surfacedKeys, - ...banners.map((banner) => banner.key), - ]); - if (merged.size > surfacedKeys.size) { - await this.secretsManager.setSurfacedBanners( - session.deployment.safeHostname, - [...merged], - ); - } + await this.mementoManager.addSurfacedBanners( + session.deployment.safeHostname, + banners.map((banner) => banner.key), + ); this.setAttentionIndicator(false); } catch (error) { this.logger.warn("Failed to mark Coder announcements as surfaced", error); diff --git a/src/core/container.ts b/src/core/container.ts index b448a1bdd..02fbd70b3 100644 --- a/src/core/container.ts +++ b/src/core/container.ts @@ -50,7 +50,7 @@ export class ServiceContainer implements vscode.Disposable { this.mementoManager = new MementoManager(context.globalState); this.secretsManager = new SecretsManager( context.secrets, - context.globalState, + this.mementoManager, this.logger, ); diff --git a/src/core/mementoManager.ts b/src/core/mementoManager.ts index b86853265..d27a15892 100644 --- a/src/core/mementoManager.ts +++ b/src/core/mementoManager.ts @@ -1,10 +1,17 @@ +import { z } from "zod"; + import type { Memento } from "vscode"; -// Maximum number of recent URLs to store. +/** Maximum number of recent URLs to store. */ const MAX_URLS = 10; -// Pending values expire after this duration to guard against stale -// state from crashes or interrupted reloads. +const DEPLOYMENT_ACCESS_PREFIX = "coder.access."; +const SURFACED_BANNERS_PREFIX = "coder.surfacedBanners."; + +const SurfacedBannersSchema = z.array(z.string()); + +/** Pending values expire after this duration to guard against stale + * state from crashes or interrupted reloads. */ const PENDING_TTL_MS = 5 * 60 * 1000; /** @@ -72,6 +79,68 @@ export class MementoManager { return value ?? "none"; } + /** Record when a deployment was last accessed, for most-recently-used ordering. */ + public async updateDeploymentAccess(safeHostname: string): Promise { + await this.memento.update( + `${DEPLOYMENT_ACCESS_PREFIX}${safeHostname}`, + new Date().toISOString(), + ); + } + + public getDeploymentAccess(safeHostname: string): string | undefined { + return this.memento.get( + `${DEPLOYMENT_ACCESS_PREFIX}${safeHostname}`, + ); + } + + /** The pre-multi-deployment URL key, read during legacy migration. */ + public getLegacyUrl(): string | undefined { + return this.memento.get("url"); + } + + public async clearLegacyUrl(): Promise { + await this.memento.update("url", undefined); + } + + public getSurfacedBanners(safeHostname: string): string[] { + const raw = this.memento.get( + `${SURFACED_BANNERS_PREFIX}${safeHostname}`, + ); + const result = SurfacedBannersSchema.safeParse(raw); + return result.success ? result.data : []; + } + + /** + * Merge banner keys into the surfaced set. The read-modify-write lives + * here so a future atomic Memento update can be adopted in one place. + */ + public async addSurfacedBanners( + safeHostname: string, + bannerKeys: readonly string[], + ): Promise { + const existing = this.getSurfacedBanners(safeHostname); + const merged = new Set([...existing, ...bannerKeys]); + if (merged.size > existing.length) { + await this.memento.update(`${SURFACED_BANNERS_PREFIX}${safeHostname}`, [ + ...merged, + ]); + } + } + + /** Clear all per-deployment state (access timestamp, surfaced banners). */ + public async clearDeploymentData(safeHostname: string): Promise { + await Promise.all([ + this.memento.update( + `${DEPLOYMENT_ACCESS_PREFIX}${safeHostname}`, + undefined, + ), + this.memento.update( + `${SURFACED_BANNERS_PREFIX}${safeHostname}`, + undefined, + ), + ]); + } + private async setStamped(key: string, value: T): Promise { await this.memento.update(key, { value, setAt: Date.now() }); } diff --git a/src/core/secretsManager.ts b/src/core/secretsManager.ts index b5cbc7419..6d40840dc 100644 --- a/src/core/secretsManager.ts +++ b/src/core/secretsManager.ts @@ -4,16 +4,16 @@ import { DeploymentSchema, type Deployment } from "../deployment/types"; import { toSafeHost } from "../util/uri"; import type { OAuth2ClientRegistrationResponse } from "coder/site/src/api/typesGenerated"; -import type { Memento, SecretStorage, Disposable } from "vscode"; +import type { SecretStorage, Disposable } from "vscode"; import type { Logger } from "../logging/logger"; +import type { MementoManager } from "./mementoManager"; + // Per-deployment keys ensure atomic operations (multiple windows writing to a // shared key could drop data) and enable proper VS Code change events. const SESSION_KEY_PREFIX = "coder.session."; const OAUTH_CLIENT_PREFIX = "coder.oauth.client."; -const DEPLOYMENT_ACCESS_PREFIX = "coder.access."; -const SURFACED_BANNERS_PREFIX = "coder.surfacedBanners."; type SecretKeyPrefix = typeof SESSION_KEY_PREFIX | typeof OAUTH_CLIENT_PREFIX; @@ -30,8 +30,6 @@ export type CurrentDeploymentState = z.infer< typeof CurrentDeploymentStateSchema >; -const SurfacedBannersSchema = z.array(z.string()); - /** * OAuth token data stored alongside session auth. * When present, indicates the session is authenticated via OAuth. @@ -56,7 +54,7 @@ export type SessionAuth = z.infer; export class SecretsManager { constructor( private readonly secrets: SecretStorage, - private readonly memento: Memento, + private readonly mementoManager: MementoManager, private readonly logger: Logger, ) {} @@ -215,11 +213,7 @@ export class SecretsManager { safeHostname: string, maxCount = DEFAULT_MAX_DEPLOYMENTS, ): Promise { - // Update this deployment's access timestamp - await this.memento.update( - `${DEPLOYMENT_ACCESS_PREFIX}${safeHostname}`, - new Date().toISOString(), - ); + await this.mementoManager.updateDeploymentAccess(safeHostname); // Prune if needed - errors here shouldn't block deployment access try { @@ -232,44 +226,17 @@ export class SecretsManager { } /** - * Clear all auth data for a deployment, including the access timestamp. + * Clear all auth data for a deployment, including its memento-backed + * state (access timestamp, surfaced banners), so both stores stay in sync. */ public async clearAllAuthData(safeHostname: string): Promise { await Promise.all([ this.clearSessionAuth(safeHostname), this.clearOAuthClientRegistration(safeHostname), - this.clearSurfacedBanners(safeHostname), - this.memento.update( - `${DEPLOYMENT_ACCESS_PREFIX}${safeHostname}`, - undefined, - ), - ]); - } - - public getSurfacedBanners(safeHostname: string): string[] { - const raw = this.memento.get( - `${SURFACED_BANNERS_PREFIX}${safeHostname}`, - ); - const result = SurfacedBannersSchema.safeParse(raw); - return result.success ? result.data : []; - } - - public async setSurfacedBanners( - safeHostname: string, - bannerKeys: readonly string[], - ): Promise { - await this.memento.update(`${SURFACED_BANNERS_PREFIX}${safeHostname}`, [ - ...bannerKeys, + this.mementoManager.clearDeploymentData(safeHostname), ]); } - private async clearSurfacedBanners(safeHostname: string): Promise { - await this.memento.update( - `${SURFACED_BANNERS_PREFIX}${safeHostname}`, - undefined, - ); - } - /** * Get all known hostnames, ordered by most recently accessed. * Derives the list from actual session secrets stored. @@ -283,9 +250,7 @@ export class SecretsManager { // Sort by access timestamp (most recent first) const withTimestamps = sessionHostnames.map((hostname) => ({ hostname, - accessedAt: - this.memento.get(`${DEPLOYMENT_ACCESS_PREFIX}${hostname}`) ?? - "", + accessedAt: this.mementoManager.getDeploymentAccess(hostname) ?? "", })); withTimestamps.sort((a, b) => b.accessedAt.localeCompare(a.accessedAt)); @@ -297,7 +262,7 @@ export class SecretsManager { * Also sets the current deployment if none exists. */ public async migrateFromLegacyStorage(): Promise { - const legacyUrl = this.memento.get("url"); + const legacyUrl = this.mementoManager.getLegacyUrl(); if (!legacyUrl) { return undefined; } @@ -305,7 +270,7 @@ export class SecretsManager { const oldToken = await this.secrets.get(LEGACY_SESSION_TOKEN_KEY); await this.secrets.delete(LEGACY_SESSION_TOKEN_KEY); - await this.memento.update("url", undefined); + await this.mementoManager.clearLegacyUrl(); const safeHostname = toSafeHost(legacyUrl); const existing = await this.getSessionAuth(safeHostname); diff --git a/src/extension.ts b/src/extension.ts index 541331562..1fabc886d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -166,7 +166,7 @@ async function doActivate( const announcementManager = new AnnouncementManager( client, deploymentManager.session, - secretsManager, + mementoManager, output, ); ctx.subscriptions.push(announcementManager); diff --git a/test/unit/announcements/manager.test.ts b/test/unit/announcements/manager.test.ts index bc678c3e9..1f6259ae2 100644 --- a/test/unit/announcements/manager.test.ts +++ b/test/unit/announcements/manager.test.ts @@ -5,7 +5,7 @@ import { AnnouncementManager, REFRESH_INTERVAL_MS, } from "@/announcements/manager"; -import { SecretsManager } from "@/core/secretsManager"; +import { MementoManager } from "@/core/mementoManager"; import { SessionStore } from "@/deployment/sessionStore"; import { @@ -13,7 +13,6 @@ import { createMockUser, flushPromises, InMemoryMemento, - InMemorySecretStorage, MockConfigurationProvider, MockProgressReporter, MockStatusBarItem, @@ -45,17 +44,13 @@ function setup() { const client = { getAppearance: vi.fn<() => Promise>() }; client.getAppearance.mockResolvedValue(appearance()); const session = new SessionStore(); - const secretsManager = new SecretsManager( - new InMemorySecretStorage(), - new InMemoryMemento(), - createMockLogger(), - ); + const mementoManager = new MementoManager(new InMemoryMemento()); const statusBar = new MockStatusBarItem(); const logger = createMockLogger(); const manager = new AnnouncementManager( client, session, - secretsManager, + mementoManager, logger, ); onTestFinished(() => manager.dispose()); @@ -64,7 +59,7 @@ function setup() { config, logger, manager, - secretsManager, + mementoManager, session, statusBar, }; @@ -137,7 +132,7 @@ describe("AnnouncementManager", () => { }); it("notifies only newly seen banners", async () => { - const { client, manager, secretsManager, session } = setup(); + const { client, manager, mementoManager, session } = setup(); nextAppearance(client, ["First", "Second"]); await signIn(session); expectInfo("Coder has 2 new deployment announcements.", "View"); @@ -148,7 +143,7 @@ describe("AnnouncementManager", () => { expectInfo("Coder has a new deployment announcement.", "View"); expect( - secretsManager.getSurfacedBanners(deployment().safeHostname), + mementoManager.getSurfacedBanners(deployment().safeHostname), ).toHaveLength(3); }); @@ -164,7 +159,7 @@ describe("AnnouncementManager", () => { }); it("suppresses popups when notifications are disabled without marking banners seen", async () => { - const { client, config, manager, secretsManager, session, statusBar } = + const { client, config, manager, mementoManager, session, statusBar } = setup(); config.set("coder.disableNotifications", true); client.getAppearance.mockResolvedValue(appearance(["Maintenance tonight"])); @@ -175,7 +170,7 @@ describe("AnnouncementManager", () => { expect(statusBar.show).toHaveBeenCalled(); expect(statusBar.text).toBe("$(megaphone) Coder"); expect( - secretsManager.getSurfacedBanners(deployment().safeHostname), + mementoManager.getSurfacedBanners(deployment().safeHostname), ).toEqual([]); expect(statusBar.backgroundColor).toEqual( new vscode.ThemeColor("statusBarItem.warningBackground"), @@ -335,7 +330,7 @@ describe("AnnouncementManager", () => { }); it("clears the attention color as soon as the preview is opened, even if the refresh that preceded it failed", async () => { - const { client, config, manager, secretsManager, session, statusBar } = + const { client, config, manager, mementoManager, session, statusBar } = setup(); config.set("coder.disableNotifications", true); client.getAppearance.mockResolvedValue(appearance(["Maintenance tonight"])); @@ -351,7 +346,7 @@ describe("AnnouncementManager", () => { expect(previewContent()).toContain("Maintenance tonight"); expect(statusBar.backgroundColor).toBeUndefined(); expect( - secretsManager.getSurfacedBanners(deployment().safeHostname), + mementoManager.getSurfacedBanners(deployment().safeHostname), ).toHaveLength(1); }); diff --git a/test/unit/api/authInterceptor.test.ts b/test/unit/api/authInterceptor.test.ts index 9da903cf0..705677806 100644 --- a/test/unit/api/authInterceptor.test.ts +++ b/test/unit/api/authInterceptor.test.ts @@ -5,6 +5,7 @@ import { type AuthRequiredHandler, AuthInterceptor, } from "@/api/authInterceptor"; +import { MementoManager } from "@/core/mementoManager"; import { SecretsManager } from "@/core/secretsManager"; import { @@ -103,7 +104,11 @@ function createTestContext() { const secretStorage = new InMemorySecretStorage(); const memento = new InMemoryMemento(); const logger = createMockLogger(); - const secretsManager = new SecretsManager(secretStorage, memento, logger); + const secretsManager = new SecretsManager( + secretStorage, + new MementoManager(memento), + logger, + ); const axiosInstance = createMockAxiosInstance(); const mockCoderApi = createMockCoderApi(axiosInstance); diff --git a/test/unit/core/mementoManager.test.ts b/test/unit/core/mementoManager.test.ts index 8ce3ee189..e92c939f5 100644 --- a/test/unit/core/mementoManager.test.ts +++ b/test/unit/core/mementoManager.test.ts @@ -66,6 +66,50 @@ describe("MementoManager", () => { }); }); + describe("surfaced banners", () => { + it("stores surfaced banner keys by safe hostname", async () => { + await mementoManager.addSurfacedBanners("example.com", ["one", "two"]); + await mementoManager.addSurfacedBanners("other.com", ["three"]); + + expect(mementoManager.getSurfacedBanners("example.com")).toEqual([ + "one", + "two", + ]); + expect(mementoManager.getSurfacedBanners("other.com")).toEqual(["three"]); + }); + + it("merges new banner keys into the surfaced set", async () => { + await mementoManager.addSurfacedBanners("example.com", ["one"]); + await mementoManager.addSurfacedBanners("example.com", ["one", "two"]); + + expect(mementoManager.getSurfacedBanners("example.com")).toEqual([ + "one", + "two", + ]); + }); + + it("ignores corrupted surfaced banner storage", async () => { + await memento.update("coder.surfacedBanners.example.com", { + bad: true, + }); + + expect(mementoManager.getSurfacedBanners("example.com")).toEqual([]); + }); + }); + + describe("deployment data", () => { + it("clears access timestamp and surfaced banners together", async () => { + await mementoManager.updateDeploymentAccess("example.com"); + await mementoManager.addSurfacedBanners("example.com", ["one"]); + expect(mementoManager.getDeploymentAccess("example.com")).toBeDefined(); + + await mementoManager.clearDeploymentData("example.com"); + + expect(mementoManager.getDeploymentAccess("example.com")).toBeUndefined(); + expect(mementoManager.getSurfacedBanners("example.com")).toEqual([]); + }); + }); + describe("startupMode", () => { it("should return the set mode and clear after read", async () => { await mementoManager.setStartupMode("start"); diff --git a/test/unit/core/secretsManager.test.ts b/test/unit/core/secretsManager.test.ts index e8acc7fe3..1166bb774 100644 --- a/test/unit/core/secretsManager.test.ts +++ b/test/unit/core/secretsManager.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { MementoManager } from "@/core/mementoManager"; import { type CurrentDeploymentState, SecretsManager, @@ -14,15 +15,17 @@ import { describe("SecretsManager", () => { let secretStorage: InMemorySecretStorage; let memento: InMemoryMemento; + let mementoManager: MementoManager; let secretsManager: SecretsManager; beforeEach(() => { vi.useRealTimers(); secretStorage = new InMemorySecretStorage(); memento = new InMemoryMemento(); + mementoManager = new MementoManager(memento); secretsManager = new SecretsManager( secretStorage, - memento, + mementoManager, createMockLogger(), ); }); @@ -170,37 +173,14 @@ describe("SecretsManager", () => { vi.useRealTimers(); }); - describe("surfaced banners", () => { - it("stores surfaced banner keys by safe hostname", async () => { - await secretsManager.setSurfacedBanners("example.com", ["one", "two"]); - await secretsManager.setSurfacedBanners("other.com", ["three"]); - - expect(secretsManager.getSurfacedBanners("example.com")).toEqual([ - "one", - "two", - ]); - expect(secretsManager.getSurfacedBanners("other.com")).toEqual([ - "three", - ]); - }); - - it("clears surfaced banner keys with auth data", async () => { - await secretsManager.setSurfacedBanners("example.com", ["one"]); - await secretsManager.setSurfacedBanners("other.com", ["two"]); - - await secretsManager.clearAllAuthData("example.com"); - - expect(secretsManager.getSurfacedBanners("example.com")).toEqual([]); - expect(secretsManager.getSurfacedBanners("other.com")).toEqual(["two"]); - }); + it("clears surfaced banner keys with auth data", async () => { + await mementoManager.addSurfacedBanners("example.com", ["one"]); + await mementoManager.addSurfacedBanners("other.com", ["two"]); - it("ignores corrupted surfaced banner storage", async () => { - await memento.update("coder.surfacedBanners.example.com", { - bad: true, - }); + await secretsManager.clearAllAuthData("example.com"); - expect(secretsManager.getSurfacedBanners("example.com")).toEqual([]); - }); + expect(mementoManager.getSurfacedBanners("example.com")).toEqual([]); + expect(mementoManager.getSurfacedBanners("other.com")).toEqual(["two"]); }); }); diff --git a/test/unit/deployment/deploymentManager.test.ts b/test/unit/deployment/deploymentManager.test.ts index b1a0d4dbf..1ee2d3064 100644 --- a/test/unit/deployment/deploymentManager.test.ts +++ b/test/unit/deployment/deploymentManager.test.ts @@ -64,8 +64,12 @@ function createTestContext() { const secretStorage = new InMemorySecretStorage(); const memento = new InMemoryMemento(); const logger = createMockLogger(); - const secretsManager = new SecretsManager(secretStorage, memento, logger); const mementoManager = new MementoManager(memento); + const secretsManager = new SecretsManager( + secretStorage, + mementoManager, + logger, + ); const contextManager = new MockContextManager(); // Configure CoderApi.create mock to return validation client diff --git a/test/unit/login/loginCoordinator.test.ts b/test/unit/login/loginCoordinator.test.ts index 0a5224142..0dc86b1b3 100644 --- a/test/unit/login/loginCoordinator.test.ts +++ b/test/unit/login/loginCoordinator.test.ts @@ -120,9 +120,13 @@ function createTestContext(telemetry?: TelemetryService) { const secretStorage = new InMemorySecretStorage(); const memento = new InMemoryMemento(); const logger = createMockLogger(); - const secretsManager = new SecretsManager(secretStorage, memento, logger); - const oauthCallback = new OAuthCallback(secretStorage, logger); const mementoManager = new MementoManager(memento); + const secretsManager = new SecretsManager( + secretStorage, + mementoManager, + logger, + ); + const oauthCallback = new OAuthCallback(secretStorage, logger); const mockCredentialManager = createMockCliCredentialManager(); const authTelemetry = new AuthTelemetry( diff --git a/test/unit/oauth/testUtils.ts b/test/unit/oauth/testUtils.ts index 58e210fb5..e0fb85614 100644 --- a/test/unit/oauth/testUtils.ts +++ b/test/unit/oauth/testUtils.ts @@ -1,6 +1,7 @@ import { AxiosError, AxiosHeaders } from "axios"; import { vi } from "vitest"; +import { MementoManager } from "@/core/mementoManager"; import { SecretsManager } from "@/core/secretsManager"; import { getHeaders } from "@/headers"; import { OAuthCallback } from "@/oauth/oauthCallback"; @@ -128,7 +129,11 @@ export function createBaseTestContext() { const secretStorage = new InMemorySecretStorage(); const memento = new InMemoryMemento(); const logger = createMockLogger(); - const secretsManager = new SecretsManager(secretStorage, memento, logger); + const secretsManager = new SecretsManager( + secretStorage, + new MementoManager(memento), + logger, + ); const oauthCallback = new OAuthCallback(secretStorage, logger); /** Sets up OAuth routes, defaulting to metadata for TEST_URL. */ diff --git a/test/unit/uri/uriHandler.test.ts b/test/unit/uri/uriHandler.test.ts index c6c084e0f..e966a4dc0 100644 --- a/test/unit/uri/uriHandler.test.ts +++ b/test/unit/uri/uriHandler.test.ts @@ -68,10 +68,14 @@ function createTestContext() { const secretStorage = new InMemorySecretStorage(); const memento = new InMemoryMemento(); const logger = createMockLogger(); - const secretsManager = new SecretsManager(secretStorage, memento, logger); + const mementoManager = new MementoManager(memento); + const secretsManager = new SecretsManager( + secretStorage, + mementoManager, + logger, + ); const oauthCallback = new OAuthCallback(secretStorage, logger); const loginCoordinator = createMockLoginCoordinator(secretsManager); - const mementoManager = new MementoManager(memento); const commands = new MockCommands(); const deploymentManager = new MockDeploymentManager();