testronaut 1.12.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,6 +24,9 @@ on:
24
24
  severities:
25
25
  description: "Comma-separated severities"
26
26
  required: true
27
+ adjustedSeverity:
28
+ description: "Optional re-rated severity (alert type only, must be lower than severities)"
29
+ required: false
27
30
  reason:
28
31
  description: "Reason for exception"
29
32
  required: true
@@ -57,6 +60,7 @@ jobs:
57
60
  INPUT_ECOSYSTEM: ${{ inputs.ecosystem }}
58
61
  INPUT_SCOPE: ${{ inputs.dependencyScope }}
59
62
  INPUT_SEVERITIES: ${{ inputs.severities }}
63
+ INPUT_ADJUSTED_SEVERITY: ${{ inputs.adjustedSeverity }}
60
64
  INPUT_REASON: ${{ inputs.reason }}
61
65
  INPUT_UNTIL: ${{ inputs.acceptedUntil }}
62
66
  INPUT_APPROVED_BY: ${{ inputs.approvedBy }}
@@ -71,6 +75,7 @@ jobs:
71
75
  ${INPUT_ECOSYSTEM:+--ecosystem "$INPUT_ECOSYSTEM"} \
72
76
  ${INPUT_SCOPE:+--dependency-scope "$INPUT_SCOPE"} \
73
77
  --severities "$INPUT_SEVERITIES" \
78
+ ${INPUT_ADJUSTED_SEVERITY:+--adjusted-severity "$INPUT_ADJUSTED_SEVERITY"} \
74
79
  --reason "$INPUT_REASON" \
75
80
  --accepted-until "$INPUT_UNTIL" \
76
81
  --approved-by "$INPUT_APPROVED_BY" \
@@ -81,6 +86,16 @@ jobs:
81
86
  uses: peter-evans/create-pull-request@v6
82
87
  with:
83
88
  commit-message: "Add vulnerability SLA exception"
84
- title: "Add vulnerability SLA exception"
85
- body: "Adds a structured Periapsis vulnerability exception. Review required before merge."
86
- branch: "periapsis/vulnerability-exception-request"
89
+ title: "Add vulnerability SLA exception (${{ inputs.exceptionType }}${{ inputs.alertNumber && format(' #{0}', inputs.alertNumber) || '' }})"
90
+ body: |
91
+ Adds a structured Periapsis vulnerability exception. Review required before merge.
92
+
93
+ - Type: ${{ inputs.exceptionType }}
94
+ - Alert: ${{ inputs.alertNumber || 'n/a' }}
95
+ - Severities: ${{ inputs.severities }}
96
+ - Adjusted severity: ${{ inputs.adjustedSeverity || 'n/a' }}
97
+ - Accepted until: ${{ inputs.acceptedUntil }}
98
+ - Evidence: ${{ inputs.evidenceRef }}
99
+
100
+ Reason: ${{ inputs.reason }}
101
+ branch: "periapsis/vulnerability-exception-${{ github.run_id }}"
@@ -0,0 +1,111 @@
1
+ name: Periapsis Vulnerability Triage
2
+
3
+ on:
4
+ schedule:
5
+ - cron: "30 14 * * *"
6
+ workflow_dispatch:
7
+ inputs:
8
+ alertNumber:
9
+ description: "Dependabot alert number (leave empty to triage pending alerts per policy.triage.automation)"
10
+ required: false
11
+ mode:
12
+ description: "Use the policy's triage.mode, or override it for this run"
13
+ required: false
14
+ type: choice
15
+ default: policy
16
+ options: [policy, recommend, apply]
17
+
18
+ permissions:
19
+ contents: write # commit applied adjustments for the exception PR
20
+ pull-requests: write # open the exception PR
21
+ vulnerability-alerts: read
22
+ actions: write # restore the triage ledger artifact; single-alert apply dispatches the exception request
23
+
24
+ # The ledger is read at the start and written at the end of each run; never run two at once.
25
+ concurrency:
26
+ group: periapsis-vulnerability-triage
27
+ cancel-in-progress: false
28
+
29
+ jobs:
30
+ periapsis-vulnerability-triage:
31
+ runs-on: ubuntu-latest
32
+ env:
33
+ SMTP_CONFIGURED: ${{ secrets.PERIAPSIS_SMTP_HOST != '' }}
34
+ steps:
35
+ - uses: actions/checkout@v4
36
+ - name: Install periapsis
37
+ run: npm install -g periapsis
38
+ - name: Restore triage ledger
39
+ env:
40
+ GH_TOKEN: ${{ github.token }}
41
+ run: |
42
+ ID=$(gh api "repos/$GITHUB_REPOSITORY/actions/artifacts?name=periapsis-triage-ledger&per_page=1" --jq '.artifacts[0] | select(.expired == false) | .id' || true)
43
+ if [ -n "$ID" ]; then
44
+ mkdir -p policy/triage
45
+ gh api "repos/$GITHUB_REPOSITORY/actions/artifacts/$ID/zip" > "$RUNNER_TEMP/ledger.zip"
46
+ unzip -o "$RUNNER_TEMP/ledger.zip" -d policy/triage
47
+ echo "Restored triage ledger from artifact $ID"
48
+ else
49
+ echo "No previous triage ledger found; starting fresh"
50
+ fi
51
+ - name: Triage vulnerability alerts
52
+ id: triage
53
+ run: |
54
+ ARGS=()
55
+ if [ -n "$ALERT_NUMBER" ]; then ARGS+=(--alert "$ALERT_NUMBER"); else ARGS+=(--pending); fi
56
+ if [ -n "$MODE" ] && [ "$MODE" != "policy" ]; then ARGS+=(--mode "$MODE"); fi
57
+ periapsis vulnerability triage "${ARGS[@]}"
58
+ env:
59
+ ALERT_NUMBER: ${{ inputs.alertNumber }}
60
+ MODE: ${{ inputs.mode }}
61
+ GITHUB_TOKEN: ${{ secrets.PERIAPSIS_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
62
+ PERIAPSIS_LLM_PROVIDER: ${{ vars.PERIAPSIS_LLM_PROVIDER }}
63
+ PERIAPSIS_LLM_MODEL: ${{ vars.PERIAPSIS_LLM_MODEL }}
64
+ PERIAPSIS_LLM_API_KEY: ${{ secrets.PERIAPSIS_LLM_API_KEY }}
65
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
66
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
67
+ GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
68
+ PERIAPSIS_SLACK_WEBHOOK: ${{ secrets.PERIAPSIS_SLACK_WEBHOOK }}
69
+ - name: Save triage ledger
70
+ if: always()
71
+ uses: actions/upload-artifact@v4
72
+ with:
73
+ name: periapsis-triage-ledger
74
+ path: policy/triage/ledger.json
75
+ if-no-files-found: ignore
76
+ retention-days: 90
77
+ - name: Upload triage reports
78
+ if: always()
79
+ uses: actions/upload-artifact@v4
80
+ with:
81
+ name: vulnerability-triage-${{ github.run_id }}
82
+ path: |
83
+ policy/triage/
84
+ !policy/triage/ledger.json
85
+ if-no-files-found: ignore
86
+ retention-days: 90
87
+ - name: Validate applied adjustments
88
+ if: steps.triage.outputs.applied != '' && steps.triage.outputs.applied != '0'
89
+ run: periapsis vulnerability validate
90
+ - name: Open exception PR for applied adjustments
91
+ if: steps.triage.outputs.applied != '' && steps.triage.outputs.applied != '0'
92
+ uses: peter-evans/create-pull-request@v6
93
+ with:
94
+ add-paths: policy/vulnerability-exceptions.json
95
+ commit-message: "Add Periapsis triage severity adjustments"
96
+ title: "Periapsis triage: ${{ steps.triage.outputs.applied }} severity adjustment(s)"
97
+ body-path: policy/triage/digest.md
98
+ branch: "periapsis/triage-adjustments-${{ github.run_id }}"
99
+ - name: Send triage email
100
+ if: always() && steps.triage.outputs.email == 'true' && env.SMTP_CONFIGURED == 'true'
101
+ uses: dawidd6/action-send-mail@v3
102
+ with:
103
+ server_address: ${{ secrets.PERIAPSIS_SMTP_HOST }}
104
+ server_port: ${{ secrets.PERIAPSIS_SMTP_PORT || 587 }}
105
+ username: ${{ secrets.PERIAPSIS_SMTP_USER }}
106
+ password: ${{ secrets.PERIAPSIS_SMTP_PASSWORD }}
107
+ subject: ${{ steps.triage.outputs.subject }}
108
+ to: ${{ secrets.PERIAPSIS_EMAIL_TO }}
109
+ from: ${{ secrets.PERIAPSIS_EMAIL_FROM || secrets.PERIAPSIS_SMTP_USER }}
110
+ body: file://${{ steps.triage.outputs.report }}
111
+ convert_markdown: true
package/README.md CHANGED
@@ -49,6 +49,10 @@ Includes:
49
49
 
50
50
  ## 📦 Installation
51
51
 
52
+ Testronaut 2.x requires **Node.js 20 or newer**. This is a breaking change from
53
+ Testronaut 1.x because the bundled Playwright runtime no longer supports Node 18.
54
+ Check your runtime with `node --version` before upgrading.
55
+
52
56
  **Global install** (recommended — use `testronaut` directly):
53
57
  ```bash
54
58
  npm install -g testronaut
package/TODO.md CHANGED
@@ -9,6 +9,10 @@
9
9
  - [ ] Treat `--json` output as a stable automation contract with documented schemas and tests for every supported command.
10
10
  - [ ] If authentication ever moves outside project config, continue reading existing `sessionToken` values and provide an opt-in migration path before deprecating legacy storage.
11
11
 
12
+ ## Browser Automation Follow-Ups
13
+
14
+ - [ ] Fix `click_text` so it prioritizes exact-text actionable elements (`button`, `a`, and elements with interactive roles) instead of clicking the first container whose descendant text includes the target. Define an explicit fallback for partial matches and add regression tests covering nested containers, duplicate labels, hidden elements, and the Add, Logout, and Report failures observed in the examples run.
15
+
12
16
  ## Session and Reporting Follow-Ups
13
17
 
14
18
  - [ ] Evaluate a true shared browser-context mode for suites that require session storage or in-memory application state; define page reset, crash recovery, and concurrency behavior first.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testronaut",
3
- "version": "1.12.2",
3
+ "version": "2.0.0",
4
4
  "main": "./runner/testronaut.js",
5
5
  "bin": {
6
6
  "testronaut": "bin/cli.js"
@@ -11,6 +11,9 @@
11
11
  "./runner/testronaut": "./runner/testronaut.js"
12
12
  },
13
13
  "type": "module",
14
+ "engines": {
15
+ "node": ">=20"
16
+ },
14
17
  "scripts": {
15
18
  "benchmark:optimization": "node ./experiments/optimizationBenchmark.js",
16
19
  "benchmark:cross-provider": "node ./experiments/crossProviderBenchmark.js",
@@ -38,7 +41,7 @@
38
41
  "jiti": "^2.7.0",
39
42
  "node-fetch": "^3.3.2",
40
43
  "openai": "^6.39.0",
41
- "playwright": "^1.54.1",
44
+ "playwright": "^1.63.0",
42
45
  "prompts": "^2.4.2"
43
46
  },
44
47
  "keywords": [
@@ -56,7 +59,7 @@
56
59
  ],
57
60
  "devDependencies": {
58
61
  "@types/node": "^26.5.0",
59
- "periapsis": "^2.0.1",
62
+ "periapsis": "^2.1.2",
60
63
  "vitest": "^4.1.11"
61
64
  },
62
65
  "homepage": "https://testronaut.app"
@@ -29,5 +29,35 @@
29
29
  "allowPackageScope": true,
30
30
  "allowRepoWideScope": true,
31
31
  "allowOrgWideScope": false
32
+ },
33
+ "triage": {
34
+ "mode": "recommend",
35
+ "notify": [
36
+ "slack",
37
+ "email"
38
+ ],
39
+ "apply": {
40
+ "minimumConfidence": "high",
41
+ "minimumConfidenceSingleStep": "medium",
42
+ "allowedReachability": [
43
+ "not_reachable",
44
+ "potentially_reachable"
45
+ ],
46
+ "approvedBy": [
47
+ "periapsis-triage"
48
+ ],
49
+ "workflow": "periapsis-vulnerability-exception-request.yml",
50
+ "ref": null
51
+ },
52
+ "automation": {
53
+ "minimumSeverity": "high",
54
+ "scopes": [
55
+ "runtime",
56
+ "development"
57
+ ],
58
+ "maxTriagesPerRun": 10,
59
+ "retriageAfterDays": 30,
60
+ "skipAlerts": []
61
+ }
32
62
  }
33
- }
63
+ }
@@ -0,0 +1,22 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ const root = path.resolve(import.meta.dirname, '..');
6
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
7
+ const lock = JSON.parse(fs.readFileSync(path.join(root, 'package-lock.json'), 'utf8'));
8
+
9
+ describe('runtime dependency compatibility', () => {
10
+ it('declares the Node runtime required by the installed Playwright version', () => {
11
+ const playwright = lock.packages['node_modules/playwright'];
12
+ expect(pkg.engines.node).toBe('>=20');
13
+ expect(playwright.engines.node).toBe('>=20');
14
+ });
15
+
16
+ it('keeps the manifest, lockfile root, and installed Playwright versions aligned', () => {
17
+ expect(pkg.dependencies.playwright).toBe('^1.63.0');
18
+ expect(lock.packages[''].dependencies.playwright).toBe(pkg.dependencies.playwright);
19
+ expect(lock.packages['node_modules/playwright'].version).toBe('1.63.0');
20
+ expect(lock.packages['node_modules/playwright-core'].version).toBe('1.63.0');
21
+ });
22
+ });
@@ -0,0 +1,69 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { afterEach, describe, expect, it, vi } from 'vitest';
5
+ import { ensureBrowsers } from '../../tools/playwrightSetup.js';
6
+
7
+ const tempDirs = [];
8
+
9
+ afterEach(() => {
10
+ delete process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD;
11
+ while (tempDirs.length) fs.rmSync(tempDirs.pop(), { recursive: true, force: true });
12
+ });
13
+
14
+ function tempProject(lockfile) {
15
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'testronaut-playwright-'));
16
+ tempDirs.push(cwd);
17
+ if (lockfile) fs.writeFileSync(path.join(cwd, lockfile), '');
18
+ return cwd;
19
+ }
20
+
21
+ describe('Playwright browser setup', () => {
22
+ it('installs the browser matching the locally resolved Playwright version', async () => {
23
+ const runCommand = vi.fn().mockResolvedValue(undefined);
24
+ const cliPath = '/project/node_modules/playwright/cli.js';
25
+
26
+ await ensureBrowsers(
27
+ { browser: 'chromium', withDeps: true },
28
+ { getInstallation: () => ({ version: '1.63.0', cliPath }), runCommand, cwd: tempProject('package-lock.json') },
29
+ );
30
+
31
+ expect(runCommand).toHaveBeenCalledOnce();
32
+ expect(runCommand).toHaveBeenCalledWith(
33
+ process.execPath,
34
+ [cliPath, 'install', 'chromium', '--with-deps'],
35
+ { PLAYWRIGHT_BROWSERS_PATH: '0' },
36
+ );
37
+ });
38
+
39
+ it.each([
40
+ ['pnpm-lock.yaml', 'pnpm', ['exec', 'playwright', 'install', 'chromium']],
41
+ ['yarn.lock', 'yarn', ['playwright', 'install', 'chromium']],
42
+ ['bun.lockb', 'bunx', ['playwright', 'install', 'chromium']],
43
+ ['package-lock.json', 'npm', ['exec', '--yes', '--', 'playwright', 'install', 'chromium']],
44
+ ])('uses the project package manager fallback for %s', async (lockfile, command, args) => {
45
+ const runCommand = vi.fn()
46
+ .mockRejectedValueOnce(new Error('npx unavailable'))
47
+ .mockResolvedValueOnce(undefined);
48
+
49
+ await ensureBrowsers(
50
+ { browser: 'chromium', withDeps: false },
51
+ {
52
+ getInstallation: () => ({ version: '1.63.0', cliPath: '/project/node_modules/playwright/cli.js' }),
53
+ runCommand,
54
+ cwd: tempProject(lockfile),
55
+ },
56
+ );
57
+
58
+ expect(runCommand).toHaveBeenNthCalledWith(2, command, args, { PLAYWRIGHT_BROWSERS_PATH: '0' });
59
+ });
60
+
61
+ it('does not invoke an installer when browser downloads are disabled', async () => {
62
+ process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = '1';
63
+ const runCommand = vi.fn();
64
+
65
+ await ensureBrowsers({}, { getInstallation: vi.fn(), runCommand, cwd: tempProject() });
66
+
67
+ expect(runCommand).not.toHaveBeenCalled();
68
+ });
69
+ });
@@ -1,23 +1,22 @@
1
1
  import { createRequire } from 'module';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
- import { promisify } from 'util';
5
- import { exec as execCb } from 'child_process';
6
-
7
- const exec = promisify(execCb);
4
+ import { spawn } from 'child_process';
8
5
 
9
6
  // Resolve from the **user project** (cwd)
10
7
  const req = createRequire(path.join(process.cwd(), 'noop.js'));
11
8
 
12
9
  // Prefer 'playwright' → '@playwright/test' → 'playwright-core' from the USER PROJECT (cwd)
13
- function getPlaywrightVersion() {
10
+ function getPlaywrightInstallation() {
14
11
  console.log('🔍 Detecting installed Playwright version (from project)…');
15
12
  const tryPkg = (name) => {
16
13
  try {
17
14
  const p = req.resolve(`${name}/package.json`);
18
- const v = JSON.parse(fs.readFileSync(p, 'utf8')).version;
19
- console.log(` • found ${name}@${v} → ${p}`);
20
- return v;
15
+ const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
16
+ const bin = typeof pkg.bin === 'string' ? pkg.bin : Object.values(pkg.bin || {})[0];
17
+ const cliPath = bin ? path.resolve(path.dirname(p), bin) : null;
18
+ console.log(` • found ${name}@${pkg.version} → ${p}`);
19
+ return { version: pkg.version, cliPath };
21
20
  } catch {
22
21
  return null;
23
22
  }
@@ -26,7 +25,7 @@ function getPlaywrightVersion() {
26
25
  tryPkg('playwright') ||
27
26
  tryPkg('@playwright/test') ||
28
27
  tryPkg('playwright-core') ||
29
- 'latest'
28
+ { version: 'latest', cliPath: null }
30
29
  );
31
30
  }
32
31
 
@@ -38,33 +37,46 @@ function pkgManagerForCwd(cwd = process.cwd()) {
38
37
  return 'npm';
39
38
  }
40
39
 
41
- async function run(cmd, extraEnv = {}) {
42
- console.log(`\n> ${cmd}`);
43
- await exec(cmd, { stdio: 'inherit', env: { ...process.env, ...extraEnv } });
40
+ async function run(command, args, extraEnv = {}) {
41
+ console.log(`\n> ${[command, ...args].join(' ')}`);
42
+ await new Promise((resolve, reject) => {
43
+ const child = spawn(command, args, {
44
+ stdio: 'inherit',
45
+ env: { ...process.env, ...extraEnv },
46
+ });
47
+ child.once('error', reject);
48
+ child.once('exit', (code, signal) => {
49
+ if (code === 0) resolve();
50
+ else reject(new Error(`${command} exited with ${signal || `code ${code}`}`));
51
+ });
52
+ });
44
53
  }
45
54
 
46
- export async function ensureBrowsers({ browser = 'chromium', withDeps = true } = {}) {
55
+ export async function ensureBrowsers(
56
+ { browser = 'chromium', withDeps = true } = {},
57
+ { getInstallation = getPlaywrightInstallation, runCommand = run, cwd = process.cwd() } = {},
58
+ ) {
47
59
  if (process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD === '1') return;
48
60
 
49
- const version = getPlaywrightVersion();
50
- const withDepsFlag = withDeps ? ' --with-deps' : '';
61
+ const installation = getInstallation();
62
+ const installArgs = ['install', browser, ...(withDeps ? ['--with-deps'] : [])];
51
63
  const localCacheEnv = { PLAYWRIGHT_BROWSERS_PATH: '0' }; // project-local cache
52
64
 
53
- const npxCmd = `npx -y playwright@${version} install ${browser}${withDepsFlag}`;
54
65
  try {
55
- await run(npxCmd, localCacheEnv);
66
+ if (!installation.cliPath) throw new Error('No local Playwright CLI was resolved');
67
+ // Execute the CLI beside the package that will launch the browser. With
68
+ // PLAYWRIGHT_BROWSERS_PATH=0, running an npx-temporary CLI downloads into
69
+ // the temporary package instead of this project and leaves launch broken.
70
+ await runCommand(process.execPath, [installation.cliPath, ...installArgs], localCacheEnv);
56
71
  } catch {
57
- const pm =
58
- fs.existsSync(path.join(process.cwd(), 'pnpm-lock.yaml')) ? 'pnpm' :
59
- fs.existsSync(path.join(process.cwd(), 'yarn.lock')) ? 'yarn' :
60
- fs.existsSync(path.join(process.cwd(), 'bun.lockb')) ? 'bun' : 'npm';
72
+ const pm = pkgManagerForCwd(cwd);
61
73
 
62
- const fallback =
63
- pm === 'pnpm' ? `pnpm exec playwright install ${browser}${withDepsFlag}` :
64
- pm === 'yarn' ? `yarn playwright install ${browser}${withDepsFlag}` :
65
- pm === 'bun' ? `bunx playwright install ${browser}${withDepsFlag}` :
66
- `npm exec --yes playwright@${version} install ${browser}${withDepsFlag}`;
74
+ const [command, args] =
75
+ pm === 'pnpm' ? ['pnpm', ['exec', 'playwright', ...installArgs]] :
76
+ pm === 'yarn' ? ['yarn', ['playwright', ...installArgs]] :
77
+ pm === 'bun' ? ['bunx', ['playwright', ...installArgs]] :
78
+ ['npm', ['exec', '--yes', '--', 'playwright', ...installArgs]];
67
79
 
68
- await run(fallback, localCacheEnv);
80
+ await runCommand(command, args, localCacheEnv);
69
81
  }
70
82
  }