From 29e89b91eb420b4efc0646e1f5eab590f3358a3d Mon Sep 17 00:00:00 2001 From: isabel zimmerman Date: Thu, 16 Jul 2026 10:12:37 -0400 Subject: [PATCH 1/6] add tests --- .gitignore | 1 + apps/vscode/build.ts | 2 +- apps/vscode/package.json | 4 +- apps/vscode/scripts/run-positron-tests.mjs | 54 +++++ .../src/test/positron/execute-cell.test.ts | 203 ++++++++++++++++++ apps/vscode/src/test/positron/index.ts | 54 +++++ .../src/test/positron/notebook-export.test.ts | 87 ++++++++ yarn.lock | 50 ++++- 8 files changed, 446 insertions(+), 9 deletions(-) create mode 100644 apps/vscode/scripts/run-positron-tests.mjs create mode 100644 apps/vscode/src/test/positron/execute-cell.test.ts create mode 100644 apps/vscode/src/test/positron/index.ts create mode 100644 apps/vscode/src/test/positron/notebook-export.test.ts diff --git a/.gitignore b/.gitignore index d87a955ed..695d00b2c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,5 +16,6 @@ public/dist .ipynb_checkpoints /.luarc.json .vscode-test +.positron-test *.vsix *.timestamp-*.mjs diff --git a/apps/vscode/build.ts b/apps/vscode/build.ts index 617776868..90e05b11b 100644 --- a/apps/vscode/build.ts +++ b/apps/vscode/build.ts @@ -19,7 +19,7 @@ import * as glob from "glob"; const args = process.argv; const dev = args[2] === "dev"; const test = args[2] === "test"; -const testFiles = glob.sync("src/test/{*.ts,fixtures/*.ts,utils/*.ts}"); +const testFiles = glob.sync("src/test/{*.ts,fixtures/*.ts,utils/*.ts,positron/*.ts}"); const testBuildOptions = { entryPoints: testFiles, diff --git a/apps/vscode/package.json b/apps/vscode/package.json index 5a8c32eb4..08827b0c1 100644 --- a/apps/vscode/package.json +++ b/apps/vscode/package.json @@ -1497,7 +1497,8 @@ "lint": "eslint src --ext ts", "build-lang": "node syntaxes/build-lang", "build-test": "yarn run build test", - "test": "yarn build-test && vscode-test" + "test": "yarn build-test && vscode-test", + "test-positron": "yarn build-test && node ./scripts/run-positron-tests.mjs" }, "dependencies": { "axios": "^1.16.0", @@ -1545,6 +1546,7 @@ "@types/uuid": "^9.0.0", "@types/vscode": "1.75.0", "@types/which": "^2.0.2", + "@posit-dev/positron-test-electron": "latest", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", "@vscode/test-cli": "^0.0.11", diff --git a/apps/vscode/scripts/run-positron-tests.mjs b/apps/vscode/scripts/run-positron-tests.mjs new file mode 100644 index 000000000..44d501748 --- /dev/null +++ b/apps/vscode/scripts/run-positron-tests.mjs @@ -0,0 +1,54 @@ +/* + * run-positron-tests.mjs + * + * Launcher for the Positron-only integration tests. Downloads (or reuses a + * cached) Positron build and runs the compiled Mocha entry point + * (`test-out/positron/index.js`) inside it, via `@posit-dev/positron-test-electron`. + * + * Run with: `yarn test-positron` (which builds the tests first). + * + * Following the VS Code extension testing pattern, the tests run with + * `--disable-extensions` (applied by the harness) so the Quarto extension is + * exercised in isolation. Positron's own API global and bundled extensions + * (language runtimes, notebook export) remain available. + * + * Set POSITRON_CHANNEL=daily to test against a daily build (default: stable). + * + * Copyright (C) 2026 by Posit Software, PBC + */ + +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; +import { runTests } from "@posit-dev/positron-test-electron"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +async function main() { + // Extension root (contains package.json); `scripts/` lives one level below it. + const extensionDevelopmentPath = path.resolve(__dirname, ".."); + + // Compiled Mocha entry point that discovers and runs the Positron tests. + const extensionTestsPath = path.resolve( + extensionDevelopmentPath, + "test-out", + "positron", + "index.js" + ); + + const code = await runTests({ + channel: process.env.POSITRON_CHANNEL === "daily" ? "daily" : "stable", + extensionDevelopmentPath, + extensionTestsPath, + // Our tests exercise Positron's bundled extensions (notebook export and the + // R/Python runtimes), so opt out of the default `--disable-extensions`. + disableExtensions: false, + }); + + process.exit(code); +} + +main().catch((err) => { + console.error("Failed to run Positron integration tests:"); + console.error(err); + process.exit(1); +}); diff --git a/apps/vscode/src/test/positron/execute-cell.test.ts b/apps/vscode/src/test/positron/execute-cell.test.ts new file mode 100644 index 000000000..16e6d3db4 --- /dev/null +++ b/apps/vscode/src/test/positron/execute-cell.test.ts @@ -0,0 +1,203 @@ +/* + * execute-cell.test.ts + * + * Positron-only integration test. + * + * Verifies the Quarto-specific behavior that runs on the way to the Positron + * runtime API. In Positron the cell executor delegates to + * `positron.runtime.executeCode` (`src/host/positron.ts`); this suite asserts + * that Quarto does the right transformations before that call: + * - it strips Quarto cell options (`#| ...`) from the executed code, and + * - it reroutes knitr Python cells through `reticulate::repl_python(...)` and + * submits them to the *R* runtime. + * Neither of these paths exists in vanilla VS Code, so they are uncovered by + * the main suite. + * + * We stub Positron's API global with a spy and assert on the call the extension + * makes, rather than depending on a live kernel actually starting (which hinges + * on the host machine having a resolvable interpreter). The extension + * re-acquires the API inside `execute()` via `tryAcquirePositronApi()`, which + * reads `globalThis.acquirePositronApi` on every call, so a stub installed + * after activation is observed by the run. + * + * Copyright (C) 2026 by Posit Software, PBC + * + * Unless you have received this program directly from Posit Software pursuant + * to the terms of a commercial license agreement with Posit Software, then + * this program is licensed to you under the terms of version 3 of the + * GNU Affero General Public License. This program is distributed WITHOUT + * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, + * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the + * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. + * + */ + +import * as assert from "assert"; +import * as vscode from "vscode"; +import { extension } from "../extension"; +import { tryAcquirePositronApi } from "@posit-dev/positron"; + +interface RuntimeCall { + method: "executeCode" | "executeInlineCell"; + args: unknown[]; +} + +suite("Positron: cell execution", function () { + this.timeout(30000); + + // `acquirePositronApi` is injected onto the global by Positron; we swap it for + // a spy during each test and must restore it afterwards. + const globalWithApi = globalThis as { acquirePositronApi?: () => unknown }; + let originalAcquire: (() => unknown) | undefined; + + teardown(function () { + if (originalAcquire !== undefined) { + globalWithApi.acquirePositronApi = originalAcquire; + originalAcquire = undefined; + } + }); + + /** + * Activate Quarto, install a spy over the Positron runtime, open `qmd` as a + * Quarto document, put the cursor on `codeLine`, run the current cell, and + * return the runtime calls the extension made. + */ + async function runCellAndCaptureCalls( + qmd: string, + codeLine: number + ): Promise { + const positron = tryAcquirePositronApi(); + assert.ok( + positron, + "Positron API should be available when running under positron-test-electron" + ); + + // Activate with the real API present so Quarto selects the Positron host. + const quarto = extension(); + if (!quarto.isActive) { + await quarto.activate(); + } + + // Spy over the runtime: forward everything except the execution methods, + // which we record instead of dispatching to a kernel. + const calls: RuntimeCall[] = []; + originalAcquire = globalWithApi.acquirePositronApi; + const realApi = originalAcquire!() as { runtime: Record }; + const fakeRuntime = new Proxy(realApi.runtime, { + get(target, prop, receiver) { + if (prop === "executeCode") { + return (...args: unknown[]) => { + calls.push({ method: "executeCode", args }); + return Promise.resolve(); + }; + } + if (prop === "executeInlineCell") { + return (...args: unknown[]) => { + calls.push({ method: "executeInlineCell", args }); + return Promise.resolve(); + }; + } + return Reflect.get(target, prop, receiver); + }, + }); + const fakeApi = new Proxy(realApi, { + get(target, prop, receiver) { + if (prop === "runtime") { + return fakeRuntime; + } + return Reflect.get(target, prop, receiver); + }, + }); + globalWithApi.acquirePositronApi = () => fakeApi; + + const doc = await vscode.workspace.openTextDocument({ + language: "quarto", + content: qmd, + }); + const editor = await vscode.window.showTextDocument(doc); + editor.selection = new vscode.Selection(codeLine, 0, codeLine, 0); + + await vscode.commands.executeCommand("quarto.runCurrentCell"); + return calls; + } + + test("submits a Python cell to executeCode as python, with cell options stripped", async function () { + const marker = `qmd_marker_${Date.now()}`; + const qmd = [ + "---", + "title: test", + "---", + "", + "```{python}", + "#| echo: false", + `${marker} = 42`, + "```", + "", + ].join("\n"); + // Cursor on the statement (line 6), i.e. below the `#|` option line. + const calls = await runCellAndCaptureCalls(qmd, 6); + + const call = calls.find( + (c) => c.method === "executeCode" && c.args[0] === "python" + ); + assert.ok( + call, + "Running the cell should call positron.runtime.executeCode('python', ...). " + + `Observed: ${describe(calls)}` + ); + + const code = String(call!.args[1]); + assert.ok( + code.includes(`${marker} = 42`), + "the cell's code should be forwarded to the runtime" + ); + assert.ok( + !code.includes("#|"), + "Quarto cell options (`#| ...`) should be stripped before execution" + ); + }); + + test("reroutes a knitr Python cell through reticulate and submits it as r", async function () { + const marker = `qmd_marker_${Date.now()}`; + const qmd = [ + "---", + "engine: knitr", + "---", + "", + "```{python}", + `${marker} = 42`, + "```", + "", + ].join("\n"); + // Cursor on the statement (line 5). + const calls = await runCellAndCaptureCalls(qmd, 5); + + // In a knitr document, Quarto sends Python to the R runtime wrapped in + // reticulate rather than to the Python runtime directly. + const call = calls.find( + (c) => c.method === "executeCode" && c.args[0] === "r" + ); + assert.ok( + call, + "A knitr Python cell should be submitted to executeCode('r', ...). " + + `Observed: ${describe(calls)}` + ); + + const code = String(call!.args[1]); + assert.ok( + code.includes("reticulate::repl_python"), + "knitr Python should be wrapped in reticulate::repl_python(...)" + ); + assert.ok( + code.includes(`${marker} = 42`), + "the original Python code should be embedded in the reticulate call" + ); + }); +}); + +/** Compact summary of observed runtime calls for assertion messages. */ +function describe(calls: RuntimeCall[]): string { + return JSON.stringify( + calls.map((c) => ({ method: c.method, language: c.args[0] })) + ); +} diff --git a/apps/vscode/src/test/positron/index.ts b/apps/vscode/src/test/positron/index.ts new file mode 100644 index 000000000..fb7bb4064 --- /dev/null +++ b/apps/vscode/src/test/positron/index.ts @@ -0,0 +1,54 @@ +/* + * index.ts + * + * Mocha entry point for the Positron-only integration tests. This module is + * loaded inside the Positron extension host by `@posit-dev/positron-test-electron` + * (see `scripts/run-positron-tests.mjs`), which requires it and calls `run()`. + * + * These tests are kept separate from the main `@vscode/test-cli` suite because + * they exercise the Positron API, which is only available when the tests run + * inside Positron rather than vanilla VS Code. + * + * Copyright (C) 2026 by Posit Software, PBC + * + * Unless you have received this program directly from Posit Software pursuant + * to the terms of a commercial license agreement with Posit Software, then + * this program is licensed to you under the terms of version 3 of the + * GNU Affero General Public License. This program is distributed WITHOUT + * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, + * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the + * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. + * + */ + +import * as path from "path"; +import * as glob from "glob"; +import Mocha from "mocha"; + +export function run(): Promise { + const mocha = new Mocha({ + ui: "tdd", + color: true, + // Runtime start-up and cross-process execution are slower than the plain + // VS Code suite, so give each test a generous ceiling. + timeout: 120000, + }); + + const testsRoot = __dirname; + const files = glob.sync("**/*.test.js", { cwd: testsRoot }); + files.forEach((file) => mocha.addFile(path.resolve(testsRoot, file))); + + return new Promise((resolve, reject) => { + try { + mocha.run((failures) => { + if (failures > 0) { + reject(new Error(`${failures} test(s) failed.`)); + } else { + resolve(); + } + }); + } catch (err) { + reject(err); + } + }); +} diff --git a/apps/vscode/src/test/positron/notebook-export.test.ts b/apps/vscode/src/test/positron/notebook-export.test.ts new file mode 100644 index 000000000..1afb34f8c --- /dev/null +++ b/apps/vscode/src/test/positron/notebook-export.test.ts @@ -0,0 +1,87 @@ +/* + * notebook-export.test.ts + * + * Positron-only integration test. + * + * Verifies that Quarto registers its notebook exporter with Positron's + * `positron.notebook-export` extension API. This is pure cross-extension wiring + * that only exists in Positron (`src/providers/notebook-export.ts`) and is not + * exercised by the vanilla VS Code suite, where the `positron.notebook-export` + * extension is absent. + * + * Copyright (C) 2026 by Posit Software, PBC + * + * Unless you have received this program directly from Posit Software pursuant + * to the terms of a commercial license agreement with Posit Software, then + * this program is licensed to you under the terms of version 3 of the + * GNU Affero General Public License. This program is distributed WITHOUT + * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, + * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the + * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. + * + */ + +import * as assert from "assert"; +import * as vscode from "vscode"; +import { extension } from "../extension"; +import { notebookExporterLabel } from "../../providers/notebook-export"; +import { NotebookExportExtension } from "../../@types/positron-notebook-export"; + +const kNotebookExportExtensionId = "positron.notebook-export"; + +suite("Positron: notebook export", function () { + test("registers the Quarto Markdown exporter with positron.notebook-export", async function () { + this.timeout(30000); + + const exportExt = vscode.extensions.getExtension( + kNotebookExportExtensionId + ); + assert.ok( + exportExt, + `Expected the built-in ${kNotebookExportExtensionId} extension to be present in Positron` + ); + + const exportApi = await exportExt.activate(); + + // Activating Quarto is what registers the exporter against the notebook + // export API. + const quarto = extension(); + if (!quarto.isActive) { + await quarto.activate(); + } + + // Quarto registers the exporter asynchronously (after the notebook-export + // extension finishes activating), so poll until it shows up rather than + // asserting immediately. + const quartoExporter = await waitFor( + () => exportApi.exporters.find((e) => e.label === notebookExporterLabel), + 15000 + ); + + assert.ok( + quartoExporter, + `Expected an exporter labelled "${notebookExporterLabel}" to be registered` + ); + assert.strictEqual( + quartoExporter.fileExtension, + ".qmd", + "Quarto exporter should target the .qmd file extension" + ); + }); +}); + +/** Poll `fn` until it returns a truthy value or `timeoutMs` elapses. */ +async function waitFor( + fn: () => T | undefined, + timeoutMs: number +): Promise { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + const value = fn(); + if (value) { + return value; + } + await new Promise((resolve) => setTimeout(resolve, 200)); + } + return fn(); +} diff --git a/yarn.lock b/yarn.lock index 6155cb248..a11bb89b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2064,6 +2064,13 @@ resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@posit-dev/positron-test-electron@latest": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@posit-dev/positron-test-electron/-/positron-test-electron-0.0.1.tgz#76c045fa186e67899c5ec179d86c92de01a7dfbd" + integrity sha512-qN3KkagyRPvIVzF/T6K9T0hHJ3x6QUAbVBw07T6AGA8KGyAVzTFn3gtsLPc38uA3dJAqBDyNLtZuj7aYHaXNDQ== + dependencies: + "@vscode/test-electron" "^2.4.1" + "@posit-dev/positron@^0.2.4": version "0.2.4" resolved "https://registry.yarnpkg.com/@posit-dev/positron/-/positron-0.2.4.tgz#7cbd2601963f4df49b45f58815c7db721c7118b6" @@ -2794,7 +2801,7 @@ supports-color "^9.4.0" yargs "^17.7.2" -"@vscode/test-electron@^2.5.2": +"@vscode/test-electron@^2.4.1", "@vscode/test-electron@^2.5.2": version "2.5.2" resolved "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.5.2.tgz" integrity sha512-8ukpxv4wYe0iWMRQU18jhzJOHkeGKbnw7xWRX3Zw1WJA4cEKbHcmmLPdPrPtL6rhDcrlCZN+xKRpv09n4gRHYg== @@ -2873,10 +2880,10 @@ acorn-walk@^8.1.1, acorn-walk@^8.2.0: dependencies: acorn "^8.11.0" -acorn@8.16.0: - version "8.16.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" - integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== +acorn@8.17.0: + version "8.17.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.17.0.tgz#1785adb84faf8d8add10369b93826fc2bd08f1fe" + integrity sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg== acorn@^7.1.0, acorn@^7.1.1, acorn@^7.4.0: version "7.4.1" @@ -8682,7 +8689,20 @@ stop-iteration-iterator@^1.1.0: es-errors "^1.3.0" internal-slot "^1.1.0" +<<<<<<< HEAD "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +======= +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +>>>>>>> 12df022f (add tests) version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -8755,7 +8775,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -9736,7 +9763,16 @@ workerpool@^9.2.0: resolved "https://registry.npmjs.org/workerpool/-/workerpool-9.3.3.tgz" integrity sha512-slxCaKbYjEdFT/o2rH9xS1hf4uRDch1w7Uo+apxhZ+sf/1d9e0ZVkn42kPNGP2dgjIx6YFvSevj0zHvbWe2jdw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From a83632a4b5e2b996d9cab72d68e13f8ce8a130af Mon Sep 17 00:00:00 2001 From: isabel zimmerman Date: Thu, 16 Jul 2026 11:18:02 -0400 Subject: [PATCH 2/6] use placeholder test workflow --- .github/workflows/test-positron.yaml | 29 ++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 30 insertions(+) create mode 100644 .github/workflows/test-positron.yaml diff --git a/.github/workflows/test-positron.yaml b/.github/workflows/test-positron.yaml new file mode 100644 index 000000000..8abef2d73 --- /dev/null +++ b/.github/workflows/test-positron.yaml @@ -0,0 +1,29 @@ +name: Test Positron API + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + test-positron: + runs-on: macos-latest + name: Test Positron API + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: latest + - uses: quarto-dev/quarto-actions/setup@v2 + - name: Build vscode extension + run: | + yarn install + yarn run build-vscode + - name: Run Positron API tests + uses: posit-dev/setup-positron@main + with: + positron-channel: stable diff --git a/package.json b/package.json index 0c7c4827c..20fb6dee3 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dev-vscode": "turbo run dev --filter quarto...", "build-vscode": "turbo run build --filter quarto...", "test-vscode": "cd apps/vscode && yarn test", + "test-positron": "cd apps/vscode && yarn test-positron", "install-vscode": "cd apps/vscode && yarn install-vscode", "install-positron": "cd apps/vscode && yarn install-positron", "lint": "turbo run lint", From 3302c12fb90fefca7d09fa7c79751bb7c30ce2c1 Mon Sep 17 00:00:00 2001 From: isabel zimmerman Date: Thu, 16 Jul 2026 11:31:55 -0400 Subject: [PATCH 3/6] Raise Node heap limit for Positron test build on macOS runner The macos-latest runner has ~7 GB RAM, so Node's default V8 old-space heap limit (~2 GB) is too small for the vscode-editor vite build, which OOMs during 'yarn run build-vscode'. The same build passes on ubuntu runners (16 GB RAM, ~4 GB default heap). Set NODE_OPTIONS to match. --- .github/workflows/test-positron.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/test-positron.yaml b/.github/workflows/test-positron.yaml index 8abef2d73..9e9008c4f 100644 --- a/.github/workflows/test-positron.yaml +++ b/.github/workflows/test-positron.yaml @@ -20,6 +20,11 @@ jobs: node-version: latest - uses: quarto-dev/quarto-actions/setup@v2 - name: Build vscode extension + # macos-latest runners have ~7 GB RAM, so Node's default V8 heap limit + # (~2 GB) is too small for the vscode-editor vite build and it OOMs. + # Raise the limit to match what larger (ubuntu) runners get by default. + env: + NODE_OPTIONS: --max-old-space-size=4096 run: | yarn install yarn run build-vscode From ac22d1c6a15ad9791acf507bf89ed5764a353e56 Mon Sep 17 00:00:00 2001 From: isabel zimmerman Date: Thu, 16 Jul 2026 11:47:00 -0400 Subject: [PATCH 4/6] Disable extension auto-update in Positron test host Running with extensions enabled (disableExtensions: false) keeps the gallery active, so Positron auto-updates 'outdated' extensions on startup and disables/removes the development Quarto extension loaded via extensionDevelopmentPath. By test time getExtension('quarto.quarto') returns undefined ('Extension quarto.quarto not found'). Seed a throwaway user-data-dir with extensions.autoUpdate / autoCheckUpdates set to false and pass it via launchArgs. positron-test- electron appends our launchArgs last, and Positron uses the last --user-data-dir, so our seeded settings win. --- apps/vscode/scripts/run-positron-tests.mjs | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/apps/vscode/scripts/run-positron-tests.mjs b/apps/vscode/scripts/run-positron-tests.mjs index 44d501748..3448fe071 100644 --- a/apps/vscode/scripts/run-positron-tests.mjs +++ b/apps/vscode/scripts/run-positron-tests.mjs @@ -17,12 +17,45 @@ * Copyright (C) 2026 by Posit Software, PBC */ +import * as fs from "node:fs"; +import * as os from "node:os"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; import { runTests } from "@posit-dev/positron-test-electron"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); +/** + * Create a throwaway user-data-dir seeded with settings that turn off extension + * auto-update, and return `--user-data-dir ` launch args pointing at it. + * + * We run with extensions enabled (see `disableExtensions: false` below) so + * Positron's bundled extensions stay available, but that also leaves the + * extension gallery active. On startup Positron then auto-updates "outdated" + * extensions, and it treats the Quarto extension we load from + * `extensionDevelopmentPath` as one of them: it disables/removes it mid-run, so + * `vscode.extensions.getExtension("quarto.quarto")` is gone by the time the + * tests query it ("Extension quarto.quarto not found"). Disabling auto-update + * keeps our development extension in place. + * + * `@posit-dev/positron-test-electron` always passes its own `--user-data-dir` + * first and appends ours last; Positron uses the last occurrence, so ours wins. + * A short `os.tmpdir()` prefix keeps the derived IPC socket path under macOS's + * 103-char Unix-socket limit. + */ +function seededUserDataDirArgs() { + const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "qpt-")); + fs.mkdirSync(path.join(userDataDir, "User"), { recursive: true }); + fs.writeFileSync( + path.join(userDataDir, "User", "settings.json"), + JSON.stringify({ + "extensions.autoUpdate": false, + "extensions.autoCheckUpdates": false, + }) + ); + return ["--user-data-dir", userDataDir]; +} + async function main() { // Extension root (contains package.json); `scripts/` lives one level below it. const extensionDevelopmentPath = path.resolve(__dirname, ".."); @@ -42,6 +75,7 @@ async function main() { // Our tests exercise Positron's bundled extensions (notebook export and the // R/Python runtimes), so opt out of the default `--disable-extensions`. disableExtensions: false, + launchArgs: seededUserDataDirArgs(), }); process.exit(code); From 16fd3c9bf69e27c067efb1b6ee936fcb2624e31c Mon Sep 17 00:00:00 2001 From: isabel zimmerman Date: Thu, 16 Jul 2026 15:23:58 -0400 Subject: [PATCH 5/6] update yarn.lock --- yarn.lock | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index a11bb89b5..8af79a82f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8689,9 +8689,6 @@ stop-iteration-iterator@^1.1.0: es-errors "^1.3.0" internal-slot "^1.1.0" -<<<<<<< HEAD -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: -======= "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" @@ -8701,8 +8698,7 @@ stop-iteration-iterator@^1.1.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: ->>>>>>> 12df022f (add tests) +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== From c5e439b32e38d238e2c9184511945bed2a088c5e Mon Sep 17 00:00:00 2001 From: isabel zimmerman Date: Thu, 16 Jul 2026 16:27:14 -0400 Subject: [PATCH 6/6] Use positron-test-electron 0.0.2 and drop the runner's user-data-dir arg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit positron-test-electron 0.0.2 disables extension auto-update inside the test host it launches (seeding its own temp user-data-dir), which keeps the development Quarto extension from being evicted mid-run. Passing a second --user-data-dir from the runner (to seed the same setting ourselves) made Positron's getUserDataPath receive an array and crash with ERR_INVALID_ARG_TYPE, so drop it — 0.0.2 covers it in-package. Pin the dependency to ^0.0.2 (was floating "latest", which had locked to the fix-less 0.0.1) so CI installs the version with the fix. --- apps/vscode/package.json | 2 +- apps/vscode/scripts/run-positron-tests.mjs | 37 ++-------------------- yarn.lock | 8 ++--- 3 files changed, 8 insertions(+), 39 deletions(-) diff --git a/apps/vscode/package.json b/apps/vscode/package.json index 08827b0c1..71f587ac8 100644 --- a/apps/vscode/package.json +++ b/apps/vscode/package.json @@ -1546,7 +1546,7 @@ "@types/uuid": "^9.0.0", "@types/vscode": "1.75.0", "@types/which": "^2.0.2", - "@posit-dev/positron-test-electron": "latest", + "@posit-dev/positron-test-electron": "^0.0.2", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", "@vscode/test-cli": "^0.0.11", diff --git a/apps/vscode/scripts/run-positron-tests.mjs b/apps/vscode/scripts/run-positron-tests.mjs index 3448fe071..b36f183e7 100644 --- a/apps/vscode/scripts/run-positron-tests.mjs +++ b/apps/vscode/scripts/run-positron-tests.mjs @@ -17,45 +17,12 @@ * Copyright (C) 2026 by Posit Software, PBC */ -import * as fs from "node:fs"; -import * as os from "node:os"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; import { runTests } from "@posit-dev/positron-test-electron"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -/** - * Create a throwaway user-data-dir seeded with settings that turn off extension - * auto-update, and return `--user-data-dir ` launch args pointing at it. - * - * We run with extensions enabled (see `disableExtensions: false` below) so - * Positron's bundled extensions stay available, but that also leaves the - * extension gallery active. On startup Positron then auto-updates "outdated" - * extensions, and it treats the Quarto extension we load from - * `extensionDevelopmentPath` as one of them: it disables/removes it mid-run, so - * `vscode.extensions.getExtension("quarto.quarto")` is gone by the time the - * tests query it ("Extension quarto.quarto not found"). Disabling auto-update - * keeps our development extension in place. - * - * `@posit-dev/positron-test-electron` always passes its own `--user-data-dir` - * first and appends ours last; Positron uses the last occurrence, so ours wins. - * A short `os.tmpdir()` prefix keeps the derived IPC socket path under macOS's - * 103-char Unix-socket limit. - */ -function seededUserDataDirArgs() { - const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "qpt-")); - fs.mkdirSync(path.join(userDataDir, "User"), { recursive: true }); - fs.writeFileSync( - path.join(userDataDir, "User", "settings.json"), - JSON.stringify({ - "extensions.autoUpdate": false, - "extensions.autoCheckUpdates": false, - }) - ); - return ["--user-data-dir", userDataDir]; -} - async function main() { // Extension root (contains package.json); `scripts/` lives one level below it. const extensionDevelopmentPath = path.resolve(__dirname, ".."); @@ -74,8 +41,10 @@ async function main() { extensionTestsPath, // Our tests exercise Positron's bundled extensions (notebook export and the // R/Python runtimes), so opt out of the default `--disable-extensions`. + // Extension auto-update (which would otherwise evict the extension loaded + // from extensionDevelopmentPath) is disabled by @posit-dev/positron-test- + // electron itself, so no extra launch args are needed here. disableExtensions: false, - launchArgs: seededUserDataDirArgs(), }); process.exit(code); diff --git a/yarn.lock b/yarn.lock index 8af79a82f..d3bfd3a3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2064,10 +2064,10 @@ resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@posit-dev/positron-test-electron@latest": - version "0.0.1" - resolved "https://registry.yarnpkg.com/@posit-dev/positron-test-electron/-/positron-test-electron-0.0.1.tgz#76c045fa186e67899c5ec179d86c92de01a7dfbd" - integrity sha512-qN3KkagyRPvIVzF/T6K9T0hHJ3x6QUAbVBw07T6AGA8KGyAVzTFn3gtsLPc38uA3dJAqBDyNLtZuj7aYHaXNDQ== +"@posit-dev/positron-test-electron@^0.0.2": + version "0.0.2" + resolved "https://registry.yarnpkg.com/@posit-dev/positron-test-electron/-/positron-test-electron-0.0.2.tgz#509f1b56cb92342bdf0b2a8e707e4919ed2ad313" + integrity sha512-betE+lg9R6qom5cHafmimF9bJ9yWUhATj9JxzRNDAPCh9CLisQ8iSxfIv+RL+x+r7frGT0H018Y7ekb4JxW2vA== dependencies: "@vscode/test-electron" "^2.4.1"