diff --git a/examples/README.md b/examples/README.md index 0379801eb..a4af62d0d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -128,13 +128,30 @@ You can copy also results from the inline executed shell: openssl rand -base64 32 ``` -## Non-Supported Languages +## Non-Shell Languages -These are shown as simple markdowns, e.g: +These are sometimes executable by default, like for python: -```py { readonly=true } -def hello(): - print("Hello World") +```py +print("Hello World") +``` + +Otherwise, execution can be set with the `interpreter` annotation, like so: + +```yaml { interpreter=cat } +config: + nested: + para: true +``` + +Non-shell scripts can also access environment variables, and are run from the current working directory: + +```sh +export YOUR_NAME=enter your name +``` + +```javascript { name=echo-hello-js } +console.log(`Hello, ${process.env.YOUR_NAME}, from ${__dirname}!`) ``` ## Curl an image @@ -159,11 +176,3 @@ With [`antonmedv/fx`](https://github.com/antonmedv/fx) you can inspect JSON file ```sh { terminalRows=20 } curl -s "https://api.marquee.activecove.com/getWeather?lat=52&lon=10" | fx ``` - -## YAML - -```yaml -config: - netsed: - para: true -``` diff --git a/package.json b/package.json index 170917164..07ab7359a 100644 --- a/package.json +++ b/package.json @@ -623,7 +623,6 @@ "test:format:fix": "prettier --write .", "test:unit": "vitest -c ./vitest.conf.ts", "test:e2e": "wdio run ./tests/e2e/wdio.conf.ts", - "pretest:e2e": "npm run download:binary", "watch": "npm run build:dev -- --watch", "vscode:prepublish": "npm run prepare-binary", "download:binary": "npm run prepare-binary", diff --git a/src/client/components/configuration/annotations.ts b/src/client/components/configuration/annotations.ts index 3659fd402..34aa19b17 100644 --- a/src/client/components/configuration/annotations.ts +++ b/src/client/components/configuration/annotations.ts @@ -145,6 +145,11 @@ export class Annotations extends LitElement { description: "Cell's canonical name for easy referencing in the CLI.", docs: 'https://docs.runme.dev/configuration#cell-options', }, + interpreter: { + description: 'Script shebang line', + // FIXME: update docs link + docs: '', + }, category: { description: 'Execute this code cell within a category.', docs: 'https://docs.runme.dev/configuration#run-all-cells-by-category', @@ -446,6 +451,7 @@ export class Annotations extends LitElement {
${this.renderTextFieldTabEntry('mimeType')}
${this.renderCategoryTabEntry('category')}
${this.renderTextFieldTabEntry('terminalRows')}
+
${this.renderTextFieldTabEntry('interpreter')}
diff --git a/src/extension/executors/runner.ts b/src/extension/executors/runner.ts index 1db290d4e..23d754aaa 100644 --- a/src/extension/executors/runner.ts +++ b/src/extension/executors/runner.ts @@ -35,7 +35,7 @@ import { toggleTerminal } from '../commands' import { NotebookCellOutputManager } from '../cell' import { closeTerminalByEnvID } from './task' -import { getCellShellPath, parseCommandSeq, getCellCwd } from './utils' +import { parseCommandSeq, getCellCwd, getCellProgram } from './utils' import { handleVercelDeployOutput, isVercelDeployScript } from './vercel' import type { IEnvironmentManager } from '.' @@ -124,8 +124,10 @@ export async function executeRunner( } } + const { programName, commandMode } = getCellProgram(exec.cell, exec.cell.notebook, execKey) + const program = await runner.createProgramSession({ - programName: getCellShellPath(exec.cell, exec.cell.notebook, execKey) ?? execKey, + programName, environment, exec: execution, envs: Object.entries(envs).map(([k, v]) => `${k}=${v}`), @@ -134,6 +136,8 @@ export async function executeRunner( tty: interactive, convertEol: !mimeType || mimeType === 'text/plain', storeLastOutput: true, + languageId: exec.cell.document.languageId, + commandMode, }) context.subscriptions.push(program) diff --git a/src/extension/executors/utils.ts b/src/extension/executors/utils.ts index cf11f4ae0..e3b14775a 100644 --- a/src/extension/executors/utils.ts +++ b/src/extension/executors/utils.ts @@ -16,9 +16,10 @@ import { import { ENV_STORE } from '../constants' import { DEFAULT_PROMPT_ENV, OutputType } from '../../constants' -import type { CellOutputPayload, Serializer } from '../../types' +import type { CellOutputPayload, Serializer, ShellType } from '../../types' import { NotebookCellOutputManager } from '../cell' import { getAnnotations, getWorkspaceFolder } from '../utils' +import { CommandMode } from '../grpc/runnerTypes' const ENV_VAR_REGEXP = /(\$\w+)/g /** @@ -206,8 +207,8 @@ export async function retrieveShellCommand( * @param execKey Used as fallback in case `$SHELL` is not present */ export function getSystemShellPath(): string | undefined -export function getSystemShellPath(execKey: string | undefined): string | undefined export function getSystemShellPath(execKey: string): string +export function getSystemShellPath(execKey?: string): string | undefined export function getSystemShellPath(execKey?: string): string | undefined { return process.env.SHELL ?? execKey } @@ -236,6 +237,62 @@ export function getCellShellPath( return getSystemShellPath(execKey) } +export function isShellLanguage(languageId: string): ShellType | undefined { + switch (languageId.toLowerCase()) { + case 'sh': + case 'bash': + case 'zsh': + case 'ksh': + case 'shell': + case 'shellscript': + return 'sh' + + case 'bat': + case 'cmd': + return 'cmd' + + case 'powershell': + case 'pwsh': + return 'powershell' + + case 'fish': + return 'fish' + + default: + return undefined + } +} + +export function getCellProgram( + cell: NotebookCell | NotebookCellData | Serializer.Cell, + notebook: NotebookData | Serializer.Notebook | NotebookDocument, + execKey: string +): { programName: string; commandMode: CommandMode } { + let result: { programName: string; commandMode: CommandMode } + const { interpreter } = getAnnotations(cell.metadata) + + if (isShellLanguage(execKey)) { + const shellPath = getCellShellPath(cell, notebook, execKey) ?? execKey + + result = { + programName: shellPath, + commandMode: CommandMode.INLINE_SHELL, + } + } else { + // TODO(mxs): make this configurable!! + result = { + programName: '', + commandMode: CommandMode.TEMP_FILE, + } + } + + if (interpreter) { + result.programName = interpreter + } + + return result +} + export async function getCellCwd( cell: NotebookCell | NotebookCellData | Serializer.Cell, notebook?: NotebookData | NotebookDocument | Serializer.Notebook, diff --git a/src/extension/kernel.ts b/src/extension/kernel.ts index d00703e04..bfe75ec67 100644 --- a/src/extension/kernel.ts +++ b/src/extension/kernel.ts @@ -39,9 +39,9 @@ import { processEnviron, isWindows, setNotebookCategories, - isShellLanguage, getTerminalRunmeId, } from './utils' +import { isShellLanguage } from './executors/utils' import './wasm/wasm_exec.js' import { IRunner, IRunnerEnvironment } from './runner' import { executeRunner } from './executors/runner' @@ -494,7 +494,7 @@ export class Kernel implements Disposable { // TODO(mxs): support windows shells !isWindows() ) { - const runScript = (execKey: string = 'sh') => + const runScript = (key: string = execKey) => executeRunner( this, this.context, @@ -503,7 +503,7 @@ export class Kernel implements Disposable { runningCell, this.messaging, uuid, - execKey, + key, outputs, this.environment, environmentManager @@ -513,9 +513,9 @@ export class Kernel implements Disposable { return false }) - if (isShellLanguage(execKey)) { + if (isShellLanguage(execKey) || !(execKey in executor)) { successfulCellExecution = await runScript(execKey) - } else if (execKey in executor) { + } else { successfulCellExecution = await executor[execKey as keyof typeof executor].call( this, exec, @@ -524,22 +524,23 @@ export class Kernel implements Disposable { runScript, environmentManager ) - } else { - this.#shebangComingSoon.open() - - successfulCellExecution = false } - } else { + } else if (execKey in executor) { /** * check if user is running experiment to execute shell via runme cli */ - successfulCellExecution = await executor[execKey as keyof typeof executor]?.call( + successfulCellExecution = await executor[execKey as keyof typeof executor].call( this, exec, runningCell, outputs ) + } else { + window.showErrorMessage('Cell language is not executable') + + successfulCellExecution = false } + TelemetryReporter.sendTelemetryEvent('cell.endExecute', { 'cell.success': successfulCellExecution?.toString(), }) diff --git a/src/extension/provider/runmeTask.ts b/src/extension/provider/runmeTask.ts index 64655de8c..7f6daee38 100644 --- a/src/extension/provider/runmeTask.ts +++ b/src/extension/provider/runmeTask.ts @@ -24,7 +24,7 @@ import { getAnnotations, getWorkspaceEnvs, prepareCmdSeq } from '../utils' import { Serializer, RunmeTaskDefinition } from '../../types' import { SerializerBase } from '../serializer' import type { IRunner, IRunnerEnvironment, RunProgramOptions } from '../runner' -import { getCellCwd, getCellShellPath, parseCommandSeq } from '../executors/utils' +import { getCellCwd, getCellProgram, parseCommandSeq } from '../executors/utils' import { Kernel } from '../kernel' type TaskOptions = Pick @@ -118,6 +118,12 @@ export class RunmeTaskProvider implements TaskProvider { const isBackground = options.isBackground || background + const { programName, commandMode } = getCellProgram( + cell, + notebook, + ('languageId' in cell && cell.languageId) || 'sh' + ) + const name = `${command}` const task = new Task( @@ -145,7 +151,7 @@ export class RunmeTaskProvider implements TaskProvider { } const runOpts: RunProgramOptions = { - programName: getCellShellPath(cell, notebook) ?? 'sh', + programName, exec: { type: 'commands', commands: commands ?? [''], @@ -156,6 +162,7 @@ export class RunmeTaskProvider implements TaskProvider { convertEol: true, envs: Object.entries(envs).map(([k, v]) => `${k}=${v}`), storeLastOutput: true, + commandMode, } const program = await runner.createProgramSession(runOpts) diff --git a/src/extension/runner.ts b/src/extension/runner.ts index b78214408..86c52714e 100644 --- a/src/extension/runner.ts +++ b/src/extension/runner.ts @@ -6,12 +6,14 @@ import { type Disposable, type TerminalDimensions, EventEmitter, + window, } from 'vscode' import { RpcError } from '@protobuf-ts/runtime-rpc' import type { DisposableAsync } from '../types' import { + CommandMode, CreateSessionRequest, ExecuteRequest, ExecuteResponse, @@ -51,6 +53,9 @@ export interface RunProgramOptions { background?: boolean convertEol?: boolean storeLastOutput?: boolean + languageId?: string + fileExtension?: string + commandMode?: CommandMode } export interface IRunner extends Disposable { @@ -407,6 +412,17 @@ export class GrpcRunnerProgramSession implements IRunnerProgramSession { this.session.responses.onError((error) => { if (error instanceof RpcError) { + if (error.message.includes('invalid LanguageId')) { + window.showErrorMessage( + // eslint-disable-next-line max-len + 'Unable to automatically execute cell. To execute this cell, set the "interpreter" field in the configuration foldout!' + ) + } + + if (error.message.includes('invalid ProgramName')) { + window.showErrorMessage(`Unable to locate interpreter "${this.opts.programName}"`) + } + console.error( 'RpcError occurred!', { @@ -712,6 +728,9 @@ export class GrpcRunnerProgramSession implements IRunnerProgramSession { terminalDimensions, background, storeLastOutput, + fileExtension, + languageId, + commandMode, }: RunProgramOptions): ExecuteRequest { if (environment && !(environment instanceof GrpcRunnerEnvironment)) { throw new Error('Expected gRPC environment!') @@ -729,6 +748,9 @@ export class GrpcRunnerProgramSession implements IRunnerProgramSession { ...(exec?.type === 'script' && { script: exec.script }), ...(terminalDimensions && { winsize: terminalDimensionsToWinsize(terminalDimensions) }), storeLastOutput, + fileExtension, + languageId, + commandMode, }) } diff --git a/src/extension/utils.ts b/src/extension/utils.ts index 452a8e3a9..1a924e227 100644 --- a/src/extension/utils.ts +++ b/src/extension/utils.ts @@ -21,13 +21,7 @@ import { v5 as uuidv5 } from 'uuid' import getPort from 'get-port' import dotenv from 'dotenv' -import { - CellAnnotations, - CellAnnotationsErrorResult, - RunmeTerminal, - Serializer, - ShellType, -} from '../types' +import { CellAnnotations, CellAnnotationsErrorResult, RunmeTerminal, Serializer } from '../types' import { SafeCellAnnotationsSchema, CellAnnotationsSchema } from '../schema' import { AuthenticationProviders, @@ -167,31 +161,6 @@ export function getKey(runningCell: vscode.TextDocument): string { return languageId } -export function isShellLanguage(languageId: string): ShellType | undefined { - switch (languageId.toLowerCase()) { - case 'sh': - case 'bash': - case 'zsh': - case 'ksh': - case 'shell': - return 'sh' - - case 'bat': - case 'cmd': - return 'cmd' - - case 'powershell': - case 'pwsh': - return 'powershell' - - case 'fish': - return 'fish' - - default: - return undefined - } -} - /** * treat cells like a series of individual commands * which need to be executed in sequence diff --git a/src/schema.ts b/src/schema.ts index 37eeff6ec..85eda5819 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -74,6 +74,7 @@ export const AnnotationSchema = { return true }, 'mime type specification invalid format') .default('text/plain'), + interpreter: z.string().optional().default(''), cwd: z.string().nonempty().optional(), category: z.preprocess( (value) => (typeof value === 'string' ? cleanAnnotation(value, ',') : value), diff --git a/tests/e2e/specs/basic.e2e.ts b/tests/e2e/specs/basic.e2e.ts index b7818154f..d84195ed2 100644 --- a/tests/e2e/specs/basic.e2e.ts +++ b/tests/e2e/specs/basic.e2e.ts @@ -83,6 +83,28 @@ describe('Runme VS Code Extension', async () => { ]) }) + it('basic hello world python execution', async () => { + const cell = await notebook.getCell('print("Hello World")') + + await cell.run() + + expect(await cell.getCellOutput(OutputType.TerminalView)).toStrictEqual([ + 'Hello World\n' + ]) + }) + + it('basic yaml example', async () => { + const cell = await notebook.getCell('config:\n nested:\n para: true') + + await cell.run() + + await tryExecuteCommand(await browser.getWorkbench(), 'focus active cell output') + + expect(await cell.getCellOutput(OutputType.TerminalView)).toStrictEqual([ + 'config:\nnested:\npara: true\n' + ]) + }) + it('more shell example', async () => { const cell = await notebook.getCell('echo "Foo 👀"\nsleep 2\necho "Bar 🕺"\nsleep 2\necho "Loo 🚀"') await cell.run() diff --git a/tests/extension/commands/index.test.ts b/tests/extension/commands/index.test.ts index daaefdf3b..8ed9d0a21 100644 --- a/tests/extension/commands/index.test.ts +++ b/tests/extension/commands/index.test.ts @@ -67,6 +67,8 @@ vi.mock('../../../src/extension/runner', () => ({ GrpcRunnerEnvironment: class { } })) +vi.mock('../../../src/extension/grpc/runnerTypes', () => ({})) + beforeEach(() => { vi.mocked(window.showWarningMessage).mockClear() vi.mocked(window.showInformationMessage).mockClear() diff --git a/tests/extension/executors/utils.test.ts b/tests/extension/executors/utils.test.ts index 0a83d7e9f..3d68f6ec6 100644 --- a/tests/extension/executors/utils.test.ts +++ b/tests/extension/executors/utils.test.ts @@ -11,9 +11,11 @@ import { parseCommandSeq, getCellShellPath, getSystemShellPath, - getCellCwd + getCellCwd, + isShellLanguage } from '../../../src/extension/executors/utils' import { getCmdSeq, getWorkspaceFolder, getAnnotations } from '../../../src/extension/utils' +import { getCellProgram } from '../../../src/extension/executors/utils' const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) @@ -35,6 +37,16 @@ vi.mock('node:fs/promises', () => ({ } })) +const COMMAND_MODE_INLINE_SHELL = 1 +const COMMAND_MODE_TEMP_FILE = 2 + +vi.mock('../../../src/extension/grpc/runnerTypes', () => ({ + CommandMode: { + INLINE_SHELL: 1, + TEMP_FILE: 2, + } +})) + beforeEach(() => { vi.mocked(window.showInputBox).mockClear() vi.mocked(window.showErrorMessage).mockClear() @@ -346,6 +358,54 @@ suite('getCellShellPath', () => { }) }) +suite('isShellLanguage', () => { + test('usual suspects', () => { + for (const shell of ['bash', 'sh', 'fish', 'ksh', 'zsh', 'shell', 'bat', 'cmd', 'powershell', 'pwsh']) { + expect(isShellLanguage(shell)).toBeTruthy() + } + }) +}) + +suite('getCellProgram', () => { + test('is inline shell for shell types', async () => { + for (const shell of ['bash', 'sh', 'fish', 'ksh', 'zsh', 'shell', 'bat', 'cmd', 'powershell', 'pwsh']) { + vi.mocked(getAnnotations).mockReturnValueOnce({} as any) + + expect(getCellProgram({ metadata: { } } as any, {} as any, shell)).toStrictEqual({ + commandMode: COMMAND_MODE_INLINE_SHELL, + programName: getSystemShellPath() + }) + } + }) + + test('is temp file for non-shell types', async () => { + vi.mocked(getAnnotations).mockReturnValueOnce({} as any) + + expect(getCellProgram({ metadata: { } } as any, {} as any, 'python')).toStrictEqual({ + commandMode: COMMAND_MODE_TEMP_FILE, + programName: '' + }) + }) + + test('respects custom interpreter in shell mode', async () => { + vi.mocked(getAnnotations).mockImplementationOnce(((x: any) => ({ interpreter: x.interpreter })) as any) + + expect(getCellProgram({ metadata: { interpreter: 'fish' } } as any, {} as any, 'sh')).toStrictEqual({ + commandMode: COMMAND_MODE_INLINE_SHELL, + programName: 'fish' + }) + }) + + test('respects custom interpreter in temp file mode', async () => { + vi.mocked(getAnnotations).mockImplementationOnce(((x: any) => ({ interpreter: x.interpreter })) as any) + + expect(getCellProgram({ metadata: { interpreter: 'bun' } } as any, {} as any, 'javascript')).toStrictEqual({ + commandMode: COMMAND_MODE_TEMP_FILE, + programName: 'bun' + }) + }) +}) + suite('getCellCwd', () => { const projectRoot = '/project' const mdFilePath = '/project/folder/DOC.md' diff --git a/tests/extension/kernel.test.ts b/tests/extension/kernel.test.ts index e0449fa66..7437f1964 100644 --- a/tests/extension/kernel.test.ts +++ b/tests/extension/kernel.test.ts @@ -24,6 +24,8 @@ vi.mock('../../src/extension/executors/index.js', () => ({ })) vi.mock('../../src/extension/runner', () => ({})) +vi.mock('../../src/extension/grpc/runnerTypes', () => ({})) + const getCells = (cnt: number, metadata: Record = {}) => ([...new Array(cnt)]).map((_, i) => ({ document: { getText: vi.fn().mockReturnValue(`Cell #${i}`) }, notebook: { @@ -208,7 +210,7 @@ suite('_doExecuteCell', () => { test('shows error window if language is not supported', async () => { const k = new Kernel({} as any) - k['runner'] = {} as any + k['runner'] = undefined k.createCellExecution = vi.fn().mockResolvedValue({ start: vi.fn(), @@ -221,10 +223,14 @@ suite('_doExecuteCell', () => { languageId: 'barfoo' } as any) - await k['_doExecuteCell']({ - document: { uri: { fsPath: '/foo/bar' } }, - metadata: { 'runme.dev/uuid': '849448b2-3c41-4323-920e-3098e71302ce' } - } as any) + try { + await k['_doExecuteCell']({ + document: { uri: { fsPath: '/foo/bar' } }, + metadata: { 'runme.dev/uuid': '849448b2-3c41-4323-920e-3098e71302ce' } + } as any) + } catch(e) { + + } expect(TelemetryReporter.sendTelemetryEvent).toHaveBeenCalledWith( 'cell.startExecute' diff --git a/tests/extension/messages/cellOutput.test.ts b/tests/extension/messages/cellOutput.test.ts index a2c676c86..b788ef6cb 100644 --- a/tests/extension/messages/cellOutput.test.ts +++ b/tests/extension/messages/cellOutput.test.ts @@ -9,6 +9,8 @@ vi.mock('vscode') vi.mock('vscode-telemetry') vi.mock('../../../src/extension/runner', () => ({})) +vi.mock('../../../src/extension/grpc/runnerTypes', () => ({})) + suite('Handle CellOutput messages', () => { const mockOutput = (type: OutputType) => { @@ -78,4 +80,4 @@ suite('Handle CellOutput messages', () => { test('Vercel cell output', async () => { return expectOutput(OutputType.vercel) }) -}) \ No newline at end of file +}) diff --git a/tests/extension/provider/annotations.test.ts b/tests/extension/provider/annotations.test.ts index bf6bd7637..204fe66f5 100644 --- a/tests/extension/provider/annotations.test.ts +++ b/tests/extension/provider/annotations.test.ts @@ -32,6 +32,7 @@ vi.mock('../../../src/extension/utils', () => ({ })) vi.mock('../../../src/extension/runner', () => ({ })) +vi.mock('../../../src/extension/grpc/runnerTypes', () => ({})) describe('Runme Annotations', () => { const kernel = new Kernel({} as any) diff --git a/tests/extension/runner.test.ts b/tests/extension/runner.test.ts index ec245a25c..26337fd45 100644 --- a/tests/extension/runner.test.ts +++ b/tests/extension/runner.test.ts @@ -2,7 +2,8 @@ import path from 'node:path' import { vi, suite, test, expect, beforeEach } from 'vitest' import { type GrpcTransport } from '@protobuf-ts/grpc-transport' -import { EventEmitter, NotebookCellKind, Position, tasks, commands, EndOfLine } from 'vscode' +import { EventEmitter, NotebookCellKind, Position, tasks, commands, EndOfLine, window } from 'vscode' +import { RpcError } from '@protobuf-ts/runtime-rpc' import { GrpcRunner, @@ -199,6 +200,10 @@ suite('grpc Runner', () => { }) suite('grpc program session', () => { + beforeEach(() => { + vi.mocked(window.showErrorMessage).mockClear() + }) + test('session dispose is called on runner dispose', async () => { const { runner, session, duplex } = await createNewSession() @@ -559,6 +564,18 @@ suite('grpc Runner', () => { expect(writeListener).toBeCalledTimes(1) expect(writeListener).toBeCalledWith('test\n') }) + + test('grpc program failure gives info message', async () => { + const { duplex } = await createNewSession() + + duplex._onError.fire(new RpcError('invalid LanguageId')) + expect(window.showErrorMessage).toBeCalledTimes(1) + + vi.mocked(window.showErrorMessage).mockClear() + + duplex._onError.fire(new RpcError('invalid ProgramName')) + expect(window.showErrorMessage).toBeCalledTimes(1) + }) }) }) diff --git a/tests/extension/serializer.test.ts b/tests/extension/serializer.test.ts index e7ce42270..5f90487cf 100644 --- a/tests/extension/serializer.test.ts +++ b/tests/extension/serializer.test.ts @@ -40,6 +40,7 @@ vi.mock('../../src/extension/languages', () => ({ default: { fromContext: vi.fn(), }, + NotebookData: class {} })) vi.mock('../../src/extension/utils', () => ({ diff --git a/tests/extension/utils.test.ts b/tests/extension/utils.test.ts index 826926430..9e91e3f68 100644 --- a/tests/extension/utils.test.ts +++ b/tests/extension/utils.test.ts @@ -24,7 +24,6 @@ import { getNotebookCategories, getNamespacedMid, bootFile, - isShellLanguage, fileOrDirectoryExists, isMultiRootWorkspace, convertEnvList, @@ -136,12 +135,6 @@ test('getKey', () => { } as any)).toBe('sh') }) -test('isShellLanguage', () => { - for (const shell of ['bash', 'sh', 'fish', 'ksh', 'zsh', 'shell', 'bat', 'cmd', 'powershell', 'pwsh']) { - expect(isShellLanguage(shell)).toBeTruthy() - } -}) - suite('getCmdShellSeq', () => { test('one command', () => { const cellText = 'deno task start' @@ -285,7 +278,8 @@ suite('#getAnnotations', () => { category: '', excludeFromRunAll: false, promptEnv: true, - 'runme.dev/uuid': '48d86c43-84a4-469d-8c78-963513b0f9d0' + 'runme.dev/uuid': '48d86c43-84a4-469d-8c78-963513b0f9d0', + interpreter: '', } ) }) @@ -306,7 +300,8 @@ suite('#getAnnotations', () => { name: 'echo-hello', promptEnv: true, excludeFromRunAll: false, - category: '' + category: '', + interpreter: '', }) }) })