Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ jobs:
extension-contract-tests:
uses: ./.github/workflows/extension-contract-tests.yaml

positron-api-tests:
uses: ./.github/workflows/positron-api-tests.yaml

connect-contract-tests:
uses: ./.github/workflows/connect-contract-tests.yaml

Expand All @@ -34,7 +37,14 @@ jobs:
# Slack notification on failure
slack-notification:
needs:
[vscode, extension-contract-tests, connect-contract-tests, package, e2e]
[
vscode,
extension-contract-tests,
positron-api-tests,
connect-contract-tests,
package,
e2e,
]
if: failure()
runs-on: ubuntu-latest
steps:
Expand All @@ -54,7 +64,14 @@ jobs:
# Slack notification when CI recovers from failure
slack-notification-resolved:
needs:
[vscode, extension-contract-tests, connect-contract-tests, package, e2e]
[
vscode,
extension-contract-tests,
positron-api-tests,
connect-contract-tests,
package,
e2e,
]
if: success()
runs-on: ubuntu-latest
steps:
Expand Down
45 changes: 45 additions & 0 deletions .github/workflows/positron-api-tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Positron-API-Tests
on: [workflow_call]
permissions:
contents: read
env:
POSITRON_CHANNEL: stable
jobs:
test:
# @posit-dev/positron-test-electron currently supports macOS only.
runs-on: macos-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: "22"
cache: "npm"
cache-dependency-path: "**/package-lock.json"
# The interpreter-discovery tests need Python and R installed so
# Positron's runtime discovery can find them.
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- uses: r-lib/actions/setup-r@v2
with:
r-version: "4.4"
- id: get-date
run: echo "date=$(/bin/date -u "+%Y%m%d")" >> $GITHUB_OUTPUT
shell: bash
- uses: actions/cache/restore@v6
id: cache
with:
path: ./extensions/vscode/.positron-test
key: positron-${{ env.POSITRON_CHANNEL }}-${{ steps.get-date.outputs.date }}
- run: npm ci --no-audit --no-fund
- name: Run Positron API tests
uses: posit-dev/setup-positron@main
with:
positron-channel: ${{ env.POSITRON_CHANNEL }}
working-directory: extensions/vscode
run: npm run test-positron
- uses: actions/cache/save@v6
if: steps.cache.outputs.cache-hit != 'true'
with:
path: ./extensions/vscode/.positron-test
key: positron-${{ env.POSITRON_CHANNEL }}-${{ steps.get-date.outputs.date }}
5 changes: 5 additions & 0 deletions .github/workflows/pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ jobs:
if: needs.detect-changes.outputs.has-code == 'true'
uses: ./.github/workflows/extension-contract-tests.yaml

positron-api-tests:
needs: detect-changes
if: needs.detect-changes.outputs.has-code == 'true'
uses: ./.github/workflows/positron-api-tests.yaml

connect-contract-tests:
needs: detect-changes
if: needs.detect-changes.outputs.has-code == 'true'
Expand Down
1 change: 1 addition & 0 deletions extensions/vscode/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ out
dist
node_modules
.vscode-test/
.positron-test/
*.vsix
4 changes: 3 additions & 1 deletion extensions/vscode/.vscode-test.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { defineConfig } from "@vscode/test-cli";

export default defineConfig({
files: "out/test/**/*.test.js",
// Only the plain VSCode suite; out/test/positron/ holds the Positron-only
// tests, which are run by `npm run test-positron` inside a Positron build.
files: "out/test/suite/**/*.test.js",
workspaceFolder: `${import.meta.dirname}/../../../../test/sample-content/fastapi-simple`,
});
5 changes: 4 additions & 1 deletion extensions/vscode/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,12 @@ npm run test-unit

# Run only Mocha integration tests (opens VSCode test instance)
npm test

# Run Positron API integration tests (downloads a Positron build; macOS only)
npm run test-positron
```

Unit tests are in `src/**/*.test.ts` (excluding `src/test/`). Integration tests using VSCode APIs are in `src/test/`.
Unit tests are in `src/**/*.test.ts` (excluding `src/test/`). Integration tests using VSCode APIs are in `src/test/suite/`. Positron-only integration tests, which run inside a real Positron build, are in `src/test/positron/` (see its README).

# Architecture

Expand Down
12 changes: 10 additions & 2 deletions extensions/vscode/esbuild.tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@ import * as esbuild from "esbuild";

async function buildTests() {
const ctx = await esbuild.context({
entryPoints: ["src/test/**/*.test.ts"],
// index.ts is the Mocha entry point for the Positron-only suite; it is
// loaded by @posit-dev/positron-test-electron (see
// scripts/run-positron-tests.mjs).
entryPoints: ["src/test/**/*.test.ts", "src/test/positron/index.ts"],
bundle: true,
format: "cjs",
minify: false,
sourcemap: true,
platform: "node",
outdir: "out/test",
external: ["vscode"],
// Pin the output layout so out/test mirrors src/test (suite/, positron/)
// regardless of which entry points exist.
outbase: "src/test",
// mocha is resolved from node_modules at runtime by the Positron suite's
// entry point; bundling it breaks its dynamic requires.
external: ["vscode", "mocha"],
tsconfig: "tsconfig.test.json",
});

Expand Down
3 changes: 3 additions & 0 deletions extensions/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,8 @@
"esbuild-tests": "node ./esbuild.tests.mjs",
"pretest": "npm run esbuild-tests && npm run esbuild-base",
"test": "vscode-test",
"pretest-positron": "npm run esbuild-tests && npm run esbuild-base",
"test-positron": "node ./scripts/run-positron-tests.mjs",
"test-unit": "vitest run",
"test-integration-interpreters": "vitest run src/interpreters/integration.test.ts",
"test-integration-inspect": "vitest run src/inspect/integration.test.ts",
Expand All @@ -664,6 +666,7 @@
"devDependencies": {
"@eslint/js": "^10.0.1",
"@posit-dev/mock-connect": "*",
"@posit-dev/positron-test-electron": "^0.0.2",
"@types/jsonwebtoken": "^9.0.10",
"@types/mocha": "^10.0.2",
"@types/node": "^22.0.0",
Expand Down
71 changes: 71 additions & 0 deletions extensions/vscode/scripts/run-positron-tests.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (C) 2026 by Posit Software, PBC.

// Launcher for the Positron-only integration tests (src/test/positron/).
//
// Downloads (or reuses a cached) Positron build and runs the compiled Mocha
// entry point (out/test/positron/index.js) inside it, via
// @posit-dev/positron-test-electron.
//
// Run with `npm run test-positron` (which builds the extension and tests
// first). Set POSITRON_CHANNEL=daily to test against a daily Positron build
// (default: stable).
//
// NOTE: the released @posit-dev/positron-test-electron supports macOS only;
// Windows/Linux support has landed upstream and is pending an npm release.

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,
"out",
"test",
"positron",
"index.js",
);

// Publisher activates on workspaceContains:/ — open the same sample project
// the plain VSCode suite uses (.vscode-test.mjs).
const workspacePath = path.resolve(
extensionDevelopmentPath,
"..",
"..",
"test",
"sample-content",
"fastapi-simple",
);

const code = await runTests({
channel: process.env.POSITRON_CHANNEL === "daily" ? "daily" : "stable",
extensionDevelopmentPath,
extensionTestsPath,
// The interpreter-discovery tests need Positron's bundled runtime
// extensions (Python, Ark/R) to register language runtimes, so opt out of
// the default --disable-extensions. Copilot is bundled too and spams the
// logs with failed GitHub auth attempts on credential-less machines, so
// disable it individually.
disableExtensions: false,
launchArgs: [
workspacePath,
"--disable-workspace-trust",
"--disable-extension",
"GitHub.copilot-chat",
],
});

process.exit(code);
}

main().catch((err) => {
console.error("Failed to run Positron integration tests:");
console.error(err);
process.exit(1);
});
5 changes: 4 additions & 1 deletion extensions/vscode/src/interpreters/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -716,7 +716,10 @@ describe("scanRPackages (real R + renv)", async () => {

describe(
"scanPythonDependencies (real Python)",
{ timeout: 30_000 },
// Scanning spawns a real Python that enumerates installed packages, which
// can approach 30s on its own under parallel suite load; match the ceiling
// used by the scanRPackages block above.
{ timeout: 120_000 },
async () => {
const python3Available = await isExecutableAvailable("python3");
const pythonAvailable =
Expand Down
61 changes: 61 additions & 0 deletions extensions/vscode/src/test/positron/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Positron API Tests

Integration tests that run the Publisher extension inside a real
[Positron](https://positron.posit.co/) build and exercise its use of the
Positron API — code paths that the plain VSCode suite (`src/test/suite/`) and
the mock-based contract tests (`test/extension-contract-tests/`) can't reach.

Part of the rollout tracked in
[posit-dev/positron#14531](https://github.com/posit-dev/positron/issues/14531).

## How it works

- `scripts/run-positron-tests.mjs` uses
[`@posit-dev/positron-test-electron`](https://github.com/posit-dev/positron-test-electron)
to download (and cache, under `.positron-test/`) a Positron build, then runs
the compiled Mocha entry point (`out/test/positron/index.js`) inside its
extension host — the Positron analog of `@vscode/test-electron`.
- `index.ts` is that entry point: it discovers `*.test.js` files in this
directory and runs them with Mocha (tdd UI).
- Tests are compiled by `esbuild.tests.mjs` along with the plain suite; the
two are kept apart by directory (`out/test/suite/` vs `out/test/positron/`).
- Positron's bundled extensions are left enabled (no `--disable-extensions`)
because runtime discovery — which Publisher's interpreter resolution relies
on — is provided by the bundled Python and Ark (R) extensions.

## Running locally

```bash
npm run test-positron # against the latest stable Positron
POSITRON_CHANNEL=daily npm run test-positron # against a daily build
```

> **Note:** the released `@posit-dev/positron-test-electron` supports **macOS
> only**; Windows/Linux support has landed upstream
> ([posit-dev/positron-test-electron#3](https://github.com/posit-dev/positron-test-electron/issues/3))
> and is pending an npm release. Until then, on other platforms rely on the
> `Positron-API-Tests` GitHub Actions workflow
> (`.github/workflows/positron-api-tests.yaml`), which runs on every PR and
> push to `main`.

The interpreter-discovery tests expect a Python and an R installation that
Positron can discover on the machine.

## Adding tests

Add a `<name>.test.ts` file in this directory using Mocha's tdd UI
(`suite`/`test`). Things to know:

- The Positron API is reached through the `acquirePositronApi()` global that
Positron injects into the extension host (typed by
`src/@types/positron.d.ts`). Publisher's own code feature-detects Positron

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be worth a note in #3647 to update this ref when we start using the @posit-dev/positron types package.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add a note in #3647

the same way (`src/utils/vscode.ts`).
- Prefer testing Publisher's real behavior at the API boundary: import the
extension source (e.g. `import { ... } from "src/utils/vscode"`) and assert
on what it sends to / receives from the live API.
- Runtime discovery is asynchronous and can be slow on cold CI machines; wait
for Positron to report a runtime before asserting on code that depends on
one (see `waitForPreferredRuntime` in `interpreter-discovery.test.ts`).
- Keep tests independent of a live kernel actually starting whenever possible
— metadata-level assertions (`getPreferredRuntime`) are much faster and less
flaky than session-level ones.
40 changes: 40 additions & 0 deletions extensions/vscode/src/test/positron/extension.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (C) 2026 by Posit Software, PBC.

// Positron-only integration test.
//
// Sanity checks for the contract Publisher depends on when running inside
// Positron: the extension host injects an `acquirePositronApi` global
// (Publisher feature-detects Positron by calling it — see
// src/utils/vscode.ts), and the Publisher extension activates.

import * as assert from "assert";
import { extensions } from "vscode";

suite("Positron: extension host", () => {
test("Positron injects the acquirePositronApi global", () => {
assert.strictEqual(
typeof acquirePositronApi,
"function",
"the extension host should provide the acquirePositronApi global",
);

const api = acquirePositronApi();
assert.ok(api, "acquirePositronApi() should return the Positron API");
assert.strictEqual(typeof api.version, "string");
assert.ok(
api.version.length > 0,
"the Positron API should report a version",
);
});

test("Publisher activates in Positron", async () => {
const publisher = extensions.getExtension("posit.publisher");
assert.ok(
publisher,
"posit.publisher should be present in the extension host",
);

await publisher.activate();
assert.ok(publisher.isActive, "Publisher should activate without error");
});
});
Loading
Loading