diff --git a/package.json b/package.json index 6995809ab..d0e66a96f 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,18 @@ "main": "./out/extension.js", "runme": { "features": { + "SignedIn": { + "enabled": true, + "conditions": { + "os": "All", + "vsCodeVersion": ">=1.58.0", + "statefulAuthRequired": true, + "enabledForExtensions": { + "stateful.platform": true, + "stateful.runme": true + } + } + }, "ForceLogin": { "enabled": true, "conditions": { @@ -88,7 +100,7 @@ "vsCodeVersion": ">=1.58.0", "enabledForExtensions": { "stateful.platform": true, - "stateful.runme": true + "stateful.runme": false } } }, diff --git a/src/client/components/terminal/index.ts b/src/client/components/terminal/index.ts index c3a8c9082..4bc2ce584 100644 --- a/src/client/components/terminal/index.ts +++ b/src/client/components/terminal/index.ts @@ -12,7 +12,7 @@ import { ClientMessages, RENDERERS, OutputType, WebViews } from '../../../consta import { closeOutput, getContext } from '../../utils' import { onClientMessage, postClientMessage } from '../../../utils/messaging' import { stripANSI } from '../../../utils/ansi' -import { APIMethod } from '../../../types' +import { APIMethod, FeatureObserver, FeatureName } from '../../../types' import type { TerminalConfiguration } from '../../../utils/configuration' import '../closeCellButton' import '../copyButton' @@ -24,12 +24,7 @@ import { CreateExtensionCellOutputMutation, UpdateCellOutputMutation, } from '../../../extension/__generated-platform__/graphql' -import { - isFeatureActive, - loadFeatureSnapshot, - FeatureObserver, - FeatureName, -} from '../../../features' +import features from '../../../features' interface IWindowSize { width: number @@ -453,7 +448,7 @@ export class TerminalView extends LitElement { switch (e.type) { case ClientMessages.featuresResponse: case ClientMessages.featuresUpdateAction: - this.featureState$ = loadFeatureSnapshot(e.output.snapshot) + this.featureState$ = features.loadSnapshot(e.output.snapshot) break case ClientMessages.activeThemeChanged: this.#updateTerminalTheme() @@ -857,7 +852,7 @@ export class TerminalView extends LitElement { }}" > ${when( - this.isSessionOutputsEnabled && isFeatureActive(FeatureName.Gist, this.featureState$), + features.isOn(FeatureName.Gist, this.featureState$), () => { return html`` }, @@ -865,7 +860,7 @@ export class TerminalView extends LitElement { )} ${when( (this.exitCode === undefined || this.exitCode === 0 || !this.platformId) && - isFeatureActive(FeatureName.Share, this.featureState$), + features.isOn(FeatureName.Share, this.featureState$), () => { return html` { return html` { - if (!ContextState.getKey(NOTEBOOK_AUTOSAVE_ON)) { + const isSignedIn = features.isOnInContextState(FeatureName.SignedIn) + const isForceLogin = features.isOnInContextState(FeatureName.ForceLogin) + + const isAutoSaveOn = ContextState.getKey(NOTEBOOK_AUTOSAVE_ON) + + if (!isSignedIn && !isAutoSaveOn) { + return Promise.resolve() + } else if (!isSignedIn && isForceLogin) { return Promise.resolve() } + await this.withLock(async () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars await this.getExecutionUnsafe(async (exec) => { @@ -550,7 +559,7 @@ export class NotebookCellOutputManager { } } - if (!GrpcSerializer.sessionOutputsEnabled() || !terminalOutput || !terminalOutputItem) { + if (!getSessionOutputs() || !terminalOutput || !terminalOutputItem) { return } diff --git a/src/extension/commands/index.ts b/src/extension/commands/index.ts index 63ea7b919..23d40c55f 100644 --- a/src/extension/commands/index.ts +++ b/src/extension/commands/index.ts @@ -37,7 +37,7 @@ import { openFileAsRunmeNotebook, promptUserSession, } from '../utils' -import { NotebookToolbarCommand, NotebookUiEvent } from '../../types' +import { NotebookToolbarCommand, NotebookUiEvent, FeatureName } from '../../types' import getLogger from '../logger' import { RecommendExtensionMessage } from '../messaging' import { @@ -53,6 +53,7 @@ import { createGist } from '../services/github/gist' import { InitializeClient } from '../api/client' import { GetUserEnvironmentsDocument } from '../__generated-platform__/graphql' import { EnvironmentManager } from '../environment/manager' +import features from '../features' const log = getLogger('Commands') @@ -388,7 +389,9 @@ export async function addToRecommendedExtensions(context: ExtensionContext) { } export async function toggleAutosave(autoSaveIsOn: boolean) { - if (autoSaveIsOn) { + const createIfNone = features.isOnInContextState(FeatureName.ForceLogin) + + if (autoSaveIsOn && createIfNone) { await promptUserSession() } return ContextState.addKey(NOTEBOOK_AUTOSAVE_ON, autoSaveIsOn) diff --git a/src/extension/extension.ts b/src/extension/extension.ts index 73deb38a9..daadacc35 100644 --- a/src/extension/extension.ts +++ b/src/extension/extension.ts @@ -14,7 +14,7 @@ import { import { TelemetryReporter } from 'vscode-telemetry' import Channel from 'tangle/webviews' -import { NotebookUiEvent, Serializer, SyncSchema } from '../types' +import { NotebookUiEvent, Serializer, SyncSchema, FeatureName } from '../types' import { getDocsUrlFor, getForceNewWindowConfig, @@ -22,7 +22,6 @@ import { getSessionOutputs, } from '../utils/configuration' import { AuthenticationProviders, WebViews } from '../constants' -import { FeatureName } from '../features' import { Kernel } from './kernel' import KernelServer from './server/kernelServer' @@ -385,10 +384,10 @@ export class RunmeExtension { context.subscriptions.push(new StatefulAuthProvider(context, uriHandler)) getGithubAuthSession(false).then((session) => { - kernel.updateFeatureState('githubAuth', !!session) + kernel.updateFeatureContext('githubAuth', !!session) }) - const createIfNone = kernel.isFeatureActive(FeatureName.ForceLogin) + const createIfNone = kernel.isFeatureOn(FeatureName.ForceLogin) const silent = createIfNone ? undefined : true getPlatformAuthSession(createIfNone, silent) @@ -425,12 +424,12 @@ export class RunmeExtension { authentication.onDidChangeSessions((e) => { if (e.provider.id === AuthenticationProviders.Stateful) { getPlatformAuthSession(false, true).then((session) => { - kernel.updateFeatureState('statefulAuth', !!session) + kernel.updateFeatureContext('statefulAuth', !!session) }) } if (e.provider.id === AuthenticationProviders.GitHub) { getGithubAuthSession(false).then((session) => { - kernel.updateFeatureState('githubAuth', !!session) + kernel.updateFeatureContext('githubAuth', !!session) }) } }) diff --git a/src/extension/features.ts b/src/extension/features.ts new file mode 100644 index 000000000..edb304ff4 --- /dev/null +++ b/src/extension/features.ts @@ -0,0 +1,19 @@ +import features, { FEATURES_CONTEXT_STATE_KEY } from '../features' +import { FeatureName } from '../types' + +import ContextState from './contextState' + +export function isOnInContextState(featureName: FeatureName): boolean { + const snapshot = ContextState.getKey(FEATURES_CONTEXT_STATE_KEY) + if (!snapshot) { + return false + } + + const featureState$ = features.loadSnapshot(snapshot) + + return features.isOn(featureName, featureState$) +} + +export default { + isOnInContextState, +} diff --git a/src/extension/kernel.ts b/src/extension/kernel.ts index 9f0c5432a..5278f2bbd 100644 --- a/src/extension/kernel.ts +++ b/src/extension/kernel.ts @@ -34,6 +34,9 @@ import { type ClientMessage, type RunmeTerminal, type Serializer, + type ExtensionName, + type FeatureContext, + FeatureName, } from '../types' import { ClientMessages, @@ -48,16 +51,7 @@ import { import { API } from '../utils/deno/api' import { postClientMessage } from '../utils/messaging' import { getNotebookExecutionOrder, registerExtensionEnvVarsMutation } from '../utils/configuration' -import { - isFeatureActive, - FeatureContext, - getFeatureSnapshot, - loadFeaturesState, - updateFeatureContext, - FEATURES_CONTEXT_STATE_KEY, - FeatureName, - ExtensionName, -} from '../features' +import features, { FEATURES_CONTEXT_STATE_KEY } from '../features' import getLogger from './logger' import executor, { @@ -204,21 +198,21 @@ export class Kernel implements Disposable { extensionId: context?.extension?.id as ExtensionName, } - this.featuresState$ = loadFeaturesState(packageJSON, featContext, this.#featuresSettings) + this.featuresState$ = features.loadState(packageJSON, featContext, this.#featuresSettings) if (this.featuresState$) { - const features = workspace.getConfiguration('runme.features') + const runmeFeatures = workspace.getConfiguration('runme.features') const featureNames = Object.keys(FeatureName).map((f) => f.toLowerCase()) if (features) { featureNames.forEach((feature) => { - if (features.has(feature)) { - this.#featuresSettings.set(feature, features.get(feature, false)) + if (runmeFeatures.has(feature)) { + this.#featuresSettings.set(feature, runmeFeatures.get(feature, false)) } }) } const subscription = this.featuresState$ - .pipe(map((_state) => getFeatureSnapshot(this.featuresState$))) + .pipe(map((_state) => features.getSnapshot(this.featuresState$))) .subscribe((snapshot) => { ContextState.addKey(FEATURES_CONTEXT_STATE_KEY, snapshot) postClientMessage(this.messaging, ClientMessages.featuresUpdateAction, { @@ -232,16 +226,16 @@ export class Kernel implements Disposable { } } - isFeatureActive(featureName: FeatureName): boolean { + isFeatureOn(featureName: FeatureName): boolean { if (!this.featuresState$) { return false } - return isFeatureActive(featureName, this.featuresState$) + return features.isOn(featureName, this.featuresState$) } - updateFeatureState(key: K, value: FeatureContext[K]) { - updateFeatureContext(this.featuresState$, key, value, this.#featuresSettings) + updateFeatureContext(key: K, value: FeatureContext[K]) { + features.updateContext(this.featuresState$, key, value, this.#featuresSettings) } registerNotebookCell(cell: NotebookCell) { @@ -631,7 +625,7 @@ export class Kernel implements Disposable { } else if (message.type.startsWith('terminal:')) { return } else if (message.type === ClientMessages.featuresRequest) { - const snapshot = getFeatureSnapshot(this.featuresState$) + const snapshot = features.getSnapshot(this.featuresState$) postClientMessage(this.messaging, ClientMessages.featuresResponse, { snapshot: snapshot, }) diff --git a/src/extension/serializer.ts b/src/extension/serializer.ts index cccbf8a07..25e6f0a07 100644 --- a/src/extension/serializer.ts +++ b/src/extension/serializer.ts @@ -24,7 +24,7 @@ import { ulid } from 'ulidx' import { maskString } from 'data-guardian' import YAML from 'yaml' -import { Serializer } from '../types' +import { FeatureName, Serializer } from '../types' import { NOTEBOOK_AUTOSAVE_ON, NOTEBOOK_HAS_OUTPUTS, @@ -37,7 +37,6 @@ import { ServerLifecycleIdentity, getServerConfigurationValue, getSessionOutputs, - isPlatformAuthEnabled, } from '../utils/configuration' import { @@ -61,6 +60,7 @@ import { getCellById } from './cell' import { IProcessInfoState } from './terminal/terminalState' import ContextState from './contextState' import * as ghost from './ai/ghost' +import * as features from './features' declare var globalThis: any const DEFAULT_LANG_ID = 'text' @@ -551,7 +551,6 @@ export class GrpcSerializer extends SerializerBase { } protected async saveNotebookOutputsByCacheId(cacheId: string): Promise { - // if session outputs are disabled, we don't write anything if (!GrpcSerializer.sessionOutputsEnabled()) { this.togglePreviewButton(false) return -1 @@ -579,18 +578,20 @@ export class GrpcSerializer extends SerializerBase { return -1 } - const sessionFile = GrpcSerializer.getOutputsUri(srcDocUri, sessionId) - if (!sessionFile) { + // Don't write to disk if authenticated and share are disabled + if ( + features.isOnInContextState(FeatureName.SignedIn) && + features.isOnInContextState(FeatureName.Share) + ) { this.togglePreviewButton(false) - return -1 + // But still return a valid bytes length so the cache keeps working + return bytes.length } - // Don't write to disk if platform auth is enabled - const isPlatform = isPlatformAuthEnabled() - if (isPlatform) { + const sessionFile = GrpcSerializer.getOutputsUri(srcDocUri, sessionId) + if (!sessionFile) { this.togglePreviewButton(false) - // But still return a valid bytes length so the cache keeps working - return bytes.length + return -1 } await workspace.fs.writeFile(sessionFile, bytes) @@ -719,7 +720,10 @@ export class GrpcSerializer extends SerializerBase { } static sessionOutputsEnabled() { - return getSessionOutputs() && ContextState.getKey(NOTEBOOK_AUTOSAVE_ON) + const isAutoSaveOn = ContextState.getKey(NOTEBOOK_AUTOSAVE_ON) + const isSessionOutputs = getSessionOutputs() + + return isSessionOutputs && isAutoSaveOn } private async cacheNotebookOutputs( diff --git a/src/extension/utils.ts b/src/extension/utils.ts index b48620155..90c4b7a80 100644 --- a/src/extension/utils.ts +++ b/src/extension/utils.ts @@ -33,6 +33,7 @@ import { NotebookAutoSaveSetting, RunmeTerminal, Serializer, + FeatureName, } from '../types' import { SafeCellAnnotationsSchema, CellAnnotationsSchema } from '../schema' import { @@ -56,13 +57,8 @@ import { getTLSEnabled, isPlatformAuthEnabled, } from '../utils/configuration' -import { - FeatureName, - FEATURES_CONTEXT_STATE_KEY, - isFeatureActive, - loadFeatureSnapshot, -} from '../features' +import features from './features' import CategoryQuickPickItem from './quickPickItems/category' import getLogger from './logger' import { Kernel } from './kernel' @@ -726,17 +722,19 @@ export async function resolveUserSession( * This only happens once. Subsequent saves will not display the prompt. * @returns AuthenticationSession */ -export async function promptUserSession(): Promise { - let session = await resolveUserSession(false) +export async function promptUserSession() { + const createIfNone = features.isOnInContextState(FeatureName.ForceLogin) + const silent = createIfNone ? undefined : true - const provider = isPlatformAuthEnabled() ? 'Stateful' : 'GitHub' - const featureState$ = loadFeatureSnapshot(ContextState.getKey(FEATURES_CONTEXT_STATE_KEY)) - const displayLoginPrompt = getLoginPrompt() && isFeatureActive(FeatureName.Share, featureState$) + const session = await getPlatformAuthSession(false, silent) + + const displayLoginPrompt = + getLoginPrompt() && createIfNone && features.isOnInContextState(FeatureName.Share) if (!session && displayLoginPrompt !== false) { const option = await window.showInformationMessage( `Securely store your cell outputs. - Sign in with ${provider} is required, do you want to proceed?`, + Sign in with Stateful is required, do you want to proceed?`, 'Yes', 'No', 'Open Settings', @@ -749,13 +747,28 @@ export async function promptUserSession(): Promise { + if (!session) { + throw new Error('You must authenticate with your Stateful account') + } + }) + .catch((error) => { + let message + if (error instanceof Error) { + message = error.message + } else { + message = String(error) + } - return session + // https://github.com/microsoft/vscode/blob/main/src/vs/workbench/api/browser/mainThreadAuthentication.ts#L238 + // throw new Error('User did not consent to login.') + // Calling again to ensure User Menu Badge + if (createIfNone && message === 'User did not consent to login.') { + getPlatformAuthSession(false) + } + }) + } } export async function checkSession(context: ExtensionContext) { diff --git a/src/features.ts b/src/features.ts index e730892f7..7a95eddcc 100644 --- a/src/features.ts +++ b/src/features.ts @@ -1,73 +1,35 @@ import { BehaviorSubject } from 'rxjs' import { satisfies } from 'semver' -export const FEATURES_CONTEXT_STATE_KEY = 'features' - -export type FeatureContext = { - os?: string - vsCodeVersion?: string - runmeVersion?: string - extensionVersion?: string - githubAuth?: boolean // `true`, `false`, or `undefined` - statefulAuth?: boolean // `true`, `false`, or `undefined` - extensionId?: ExtensionName -} - -export enum ExtensionName { - StatefulRunme = 'stateful.runme', - StatefulPlatform = 'stateful.platform', -} - -type EnabledForExtensions = Partial> - -export type FeatureCondition = { - os?: string - vsCodeVersion?: string - runmeVersion?: string - extensionVersion?: string - githubAuthRequired?: boolean - statefulAuthRequired?: boolean - enabledForExtensions?: EnabledForExtensions -} - -export enum FeatureName { - Gist = 'Gist', - Share = 'Share', - Escalate = 'Escalate', - ForceLogin = 'ForceLogin', -} +import { + EnabledForExtensions, + ExtensionName, + Feature, + FeatureContext, + FeatureName, + FeatureObserver, + Features, + FeatureState, +} from './types' -export type Feature = { - enabled: boolean - activated: boolean - conditions: FeatureCondition -} - -export type Features = Partial> - -export type FeatureState = { - features: Features - context?: FeatureContext -} - -export type FeatureObserver = BehaviorSubject +export const FEATURES_CONTEXT_STATE_KEY = 'features' -function loadFeaturesFromPackageJson(packageJSON: any): Features { +function loadFromPackageJson(packageJSON: any): Features { const features = (packageJSON?.runme?.features || {}) as Features return features } -export function loadFeaturesState( +function loadState( packageJSON: any, context?: FeatureContext, overrides: Map = new Map(), ): FeatureObserver { - const initialFeatures = loadFeaturesFromPackageJson(packageJSON) + const initialFeatures = loadFromPackageJson(packageJSON) const state = new BehaviorSubject({ features: initialFeatures, context, }) - updateFeatureState(state, context, overrides) + updateState(state, context, overrides) return state } @@ -187,7 +149,7 @@ function isActive( if (!checkExtensionId(enabledForExtensions, context?.extensionId)) { console.log( - `Feature "${featureName}" is inactive due to checkExtensionId. Expected: ${enabledForExtensions}, actual: ${context?.extensionId}`, + `Feature "${featureName}" is inactive due to checkExtensionId. Expected: ${JSON.stringify(enabledForExtensions)}, actual: ${context?.extensionId}`, ) return false } @@ -196,7 +158,7 @@ function isActive( return true } -export function updateFeatureState( +function updateState( featureState$: FeatureObserver, context?: FeatureContext, overrides: Map = new Map(), @@ -208,7 +170,7 @@ export function updateFeatureState( ...acc, [key]: { ...value, - activated: isActive(key as FeatureName, value, context, overrides), + on: isActive(key as FeatureName, value, context, overrides), }, }), {} as Features, @@ -218,7 +180,7 @@ export function updateFeatureState( return featureState$ } -export function updateFeatureContext( +function updateContext( featureState$: FeatureObserver | undefined, key: K, value: FeatureContext[K], @@ -231,11 +193,11 @@ export function updateFeatureContext( const newContext = { ...(currentState.context || {}), [key]: value } if (newContext[key] !== currentState?.context?.[key]) { - updateFeatureState(featureState$, newContext, overrides) + updateState(featureState$, newContext, overrides) } } -export function getFeatureSnapshot(featureState$: FeatureObserver | undefined): string { +function getSnapshot(featureState$: FeatureObserver | undefined): string { if (!featureState$) { return '' } @@ -243,7 +205,7 @@ export function getFeatureSnapshot(featureState$: FeatureObserver | undefined): return JSON.stringify(featureState$.getValue()) } -export function loadFeatureSnapshot(snapshot: string): FeatureObserver { +function loadSnapshot(snapshot: string): FeatureObserver { const { features, context } = JSON.parse(snapshot) const featureState$ = new BehaviorSubject({ features, @@ -254,14 +216,20 @@ export function loadFeatureSnapshot(snapshot: string): FeatureObserver { return featureState$ } -export function isFeatureActive( - featureName: FeatureName, - featureState$?: FeatureObserver, -): boolean { +function isOn(featureName: FeatureName, featureState$?: FeatureObserver): boolean { if (!featureState$) { return false } const feature = featureState$.getValue().features[featureName] - return feature ? feature.activated : false + return feature?.on ?? false +} + +export default { + loadState, + updateState, + updateContext, + getSnapshot, + loadSnapshot, + isOn, } diff --git a/src/types.ts b/src/types.ts index f2f07c806..6310d7768 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,6 +19,7 @@ import { } from '@aws-sdk/client-ec2' import { google } from '@google-cloud/run/build/protos/protos' import { protos } from '@google-cloud/compute' +import { type BehaviorSubject } from 'rxjs' import { OutputType, ClientMessages, RUNME_FRONTMATTER_PARSED } from './constants' import { SafeCellAnnotationsSchema, SafeNotebookAnnotationsSchema } from './schema' @@ -732,3 +733,53 @@ export type NotebookUiEvent = { export type Settings = { docsUrl?: string } + +export type FeatureContext = { + os?: string + vsCodeVersion?: string + runmeVersion?: string + extensionVersion?: string + githubAuth?: boolean // `true`, `false`, or `undefined` + statefulAuth?: boolean // `true`, `false`, or `undefined` + extensionId?: ExtensionName +} + +export enum ExtensionName { + StatefulRunme = 'stateful.runme', + StatefulPlatform = 'stateful.platform', +} + +export type EnabledForExtensions = Partial> + +export type FeatureCondition = { + os?: string + vsCodeVersion?: string + runmeVersion?: string + extensionVersion?: string + githubAuthRequired?: boolean + statefulAuthRequired?: boolean + enabledForExtensions?: EnabledForExtensions +} + +export enum FeatureName { + Gist = 'Gist', + Share = 'Share', + Escalate = 'Escalate', + ForceLogin = 'ForceLogin', + SignedIn = 'SignedIn', +} + +export type Feature = { + enabled: boolean + on: boolean + conditions: FeatureCondition +} + +export type Features = Partial> + +export type FeatureState = { + features: Features + context?: FeatureContext +} + +export type FeatureObserver = BehaviorSubject diff --git a/tests/extension/cell.test.ts b/tests/extension/cell.test.ts index a91888f79..4ab07fa56 100644 --- a/tests/extension/cell.test.ts +++ b/tests/extension/cell.test.ts @@ -26,10 +26,12 @@ vi.mock('vscode', async () => { vi.mock('vscode-telemetry') vi.mock('../../src/extension/grpc/client', () => ({})) -vi.mock('../../../src/extension/grpc/runner/v1', () => ({ +vi.mock('../../src/extension/grpc/runner/v1', () => ({ ResolveProgramRequest_Mode: vi.fn(), })) +vi.mock('../../src/extension/features') + describe('NotebookCellManager', () => { it('can register cells', async () => { const manager = new NotebookCellManager({} as any) diff --git a/tests/extension/features.test.ts b/tests/extension/features.test.ts index a80d7029c..9d5c80bf8 100644 --- a/tests/extension/features.test.ts +++ b/tests/extension/features.test.ts @@ -1,17 +1,13 @@ -import { describe, it, expect, beforeEach } from 'vitest' +import { describe, it, expect, beforeEach, vi } from 'vitest' +import features from '../../src/features' import { - updateFeatureState, - getFeatureSnapshot, - loadFeaturesState, - isFeatureActive, - loadFeatureSnapshot, + ExtensionName, FeatureContext, - FeatureState, - FeatureObserver, FeatureName, - ExtensionName, -} from '../../src/features' + FeatureObserver, + FeatureState, +} from '../../src/types' const packageJSON = { runme: { @@ -50,11 +46,14 @@ const packageJSON = { }, } +vi.mock('vscode') +vi.mock('../../src/extension/contextState') + describe('Feature Store', () => { let featureState$: FeatureObserver beforeEach(() => { - featureState$ = loadFeaturesState(packageJSON, { + featureState$ = features.loadState(packageJSON, { os: 'linux', }) }) @@ -70,12 +69,12 @@ describe('Feature Store', () => { extensionId: ExtensionName.StatefulRunme, } - updateFeatureState(featureState$, initialContext) + features.updateState(featureState$, initialContext) const currentFeatures = (featureState$.getValue() as FeatureState).features - expect(currentFeatures.Escalate?.activated).toBe(false) - expect(currentFeatures.Gist?.activated).toBe(true) + expect(currentFeatures.Escalate?.on).toBe(false) + expect(currentFeatures.Gist?.on).toBe(true) }) it('should take a snapshot of the current state', () => { @@ -86,11 +85,11 @@ describe('Feature Store', () => { runmeVersion: '1.3.0', githubAuth: true, statefulAuth: true, - extensionId: 'stateful.runme', + extensionId: ExtensionName.StatefulRunme, } - updateFeatureState(featureState$, initialContext) - const snapshot = getFeatureSnapshot(featureState$) + features.updateState(featureState$, initialContext) + const snapshot = features.getSnapshot(featureState$) expect(snapshot).toBe(JSON.stringify(featureState$.getValue())) }) @@ -106,15 +105,15 @@ describe('Feature Store', () => { extensionId: ExtensionName.StatefulRunme, } - updateFeatureState(featureState$, initialContext) - const snapshot = getFeatureSnapshot(featureState$) + features.updateState(featureState$, initialContext) + const snapshot = features.getSnapshot(featureState$) featureState$.next({ context: initialContext, features: { Escalate: { enabled: true, - activated: false, + on: false, conditions: { os: 'All', vsCodeVersion: '>=1.58.0', @@ -125,7 +124,7 @@ describe('Feature Store', () => { }, Gist: { enabled: true, - activated: false, + on: false, conditions: { os: 'win32', vsCodeVersion: '>=1.60.0', @@ -136,7 +135,7 @@ describe('Feature Store', () => { }, }) - const featureStateCopy$ = loadFeatureSnapshot(snapshot) + const featureStateCopy$ = features.loadSnapshot(snapshot) const restoredFeatures = featureStateCopy$.getValue() expect(restoredFeatures).toEqual(JSON.parse(snapshot)) @@ -152,11 +151,11 @@ describe('Feature Store', () => { statefulAuth: false, extensionId: ExtensionName.StatefulRunme, } - updateFeatureState(featureState$, newContext) + features.updateState(featureState$, newContext) const currentFeatures = (featureState$.getValue() as FeatureState).features - expect(currentFeatures.Escalate?.activated).toBe(false) - expect(currentFeatures.Gist?.activated).toBe(false) + expect(currentFeatures.Escalate?.on).toBe(false) + expect(currentFeatures.Gist?.on).toBe(false) }) it('should correctly identify if a feature is enabled by name', () => { @@ -169,10 +168,10 @@ describe('Feature Store', () => { statefulAuth: true, extensionId: ExtensionName.StatefulPlatform, } - updateFeatureState(featureState$, ctx) + features.updateState(featureState$, ctx) - expect(isFeatureActive(FeatureName.Escalate, featureState$)).toBe(true) - expect(isFeatureActive(FeatureName.Gist, featureState$)).toBe(false) + expect(features.isOn(FeatureName.Escalate, featureState$)).toBe(true) + expect(features.isOn(FeatureName.Gist, featureState$)).toBe(false) }) it('should correctly identify if a feature is enabled by extensionId', () => { @@ -185,9 +184,9 @@ describe('Feature Store', () => { statefulAuth: true, extensionId: ExtensionName.StatefulRunme, } - updateFeatureState(featureState$, ctx) + features.updateState(featureState$, ctx) - expect(isFeatureActive(FeatureName.Escalate, featureState$)).toBe(false) - expect(isFeatureActive(FeatureName.Gist, featureState$)).toBe(true) + expect(features.isOn(FeatureName.Escalate, featureState$)).toBe(false) + expect(features.isOn(FeatureName.Gist, featureState$)).toBe(true) }) }) diff --git a/tests/extension/serializer.test.ts b/tests/extension/serializer.test.ts index fb1dade68..5394c3ec7 100644 --- a/tests/extension/serializer.test.ts +++ b/tests/extension/serializer.test.ts @@ -67,6 +67,8 @@ vi.mock('../../src/extension/utils', () => ({ initWasm: vi.fn(), })) +vi.mock('../../src/extension/features') + function newKernel(): Kernel { return {} as unknown as Kernel }