Compare commits

..

11 Commits

Author SHA1 Message Date
peptiderepo
6adf255dea fix(ci): P2-A injection, P2-B lint-js fallback, P2-C phpcs config detection
P2-A: move ${{ inputs.line_limit_paths }} from raw shell into env: block (PATHS)
      to eliminate template-injection surface on caller-controlled input.
P2-B: replace 'npm run lint --if-present' (always exits 0 on missing script,
      killing the node --check fallback) with explicit node -e detection;
      fallback now actually runs when no lint npm script is defined.
P2-C: both phpcbf and phpcs steps now detect which of phpcs.xml /
      phpcs.xml.dist / .phpcs.xml.dist is present and pass it to --standard;
      guard and command now agree for all three config filename variants.

Validators: actionlint 1.7.4 clean, yamllint 1.38.0 clean (both files).
Version: 1.6.1

Agent-Session: claude/reusable-ci-20260616-p2-fixes
2026-06-16 12:01:53 +00:00
peptiderepo
47acc85a0e fix(ci): graceful PHPCS skip when phpcs.xml not present
Add guard to phpcbf and phpcs strict steps: if neither phpcs.xml,
phpcs.xml.dist, nor .phpcs.xml.dist exists in the repo root, both steps
exit 0 with an info message. This prevents startup_failure on the self-test
(peptide-e2e is a Node repo with no PHPCS config) and provides clean
feedback for repos not yet configured for PHPCS.

Real PHP repos all have a phpcs.xml (prautoblogger, theme, pr-core, etc.)
so the guard is transparent for them.

Agent-Session: claude/reusable-ci-20260616
2026-06-16 11:46:12 +00:00
peptiderepo
d0f8c1d65c fix(ci): permissions model — no top-level perms in ci.yml; callers need write
GitHub Actions permission model for reusable workflows: callers must declare
at least the permission level that any job in the called workflow requests.
Since the phpcs job declares 'permissions: contents: write' (for phpcbf
commit-back), all callers must include 'permissions: contents: write' at
their workflow level.

Changes:
- ci.yml: remove top-level permissions block (was: contents: read); phpcs job
  already has job-level contents: write. Docblock now explains the caller
  requirement clearly.
- ci-self-test.yml: add 'permissions: contents: write' at workflow level.
- README.md: adoption snippet now includes the required permissions block.
- Remove diagnostic bisect/minimal test workflows (used to identify the root
  cause of startup_failure; finding: job-level write in reusable workflow
  requires caller to declare write at workflow level).

Confirmed: ci-bisect-caller with permissions: contents: write = SUCCESS.
Confirmed: actionlint 1.7.4 + yamllint 1.38.0 both clean.

Agent-Session: claude/reusable-ci-20260616
2026-06-16 11:44:24 +00:00
peptiderepo
fa1df001ea test: bisect v3 — caller has write, called has only job-level write
Agent-Session: claude/reusable-ci-20260616
2026-06-16 11:42:38 +00:00
peptiderepo
d0f4844af8 test: bisect v2 — no top-level perms in called, job-level write only
Agent-Session: claude/reusable-ci-20260616
2026-06-16 11:41:35 +00:00
peptiderepo
3fb920e343 fix(ci): move permissions: contents: write to phpcs job level
Workflow-level permissions: write in a reusable workflow causes startup_failure
when calling workflow has default (read-only) permissions. Move write scope to
the phpcs job level only — the one job that actually needs it (phpcbf commit-back).
All other jobs run with the inherited read scope.

Self-test caller (ci-self-test.yml) confirmed clean with no top-level write
permission declared — job-level override in the reusable workflow is sufficient.

Confirmed via bisect: ci-bisect.yml (workflow-level write) startup_failed;
ci-minimal.yml (no write needed) succeeded; ci-bisect-caller with
permissions: write added succeeded. Job-level approach is the correct pattern.

Agent-Session: claude/reusable-ci-20260616
2026-06-16 11:40:29 +00:00
peptiderepo
49daffafb6 test: bisect — add permissions: contents: write to caller
Agent-Session: claude/reusable-ci-20260616
2026-06-16 11:38:16 +00:00
peptiderepo
be686bb3e0 test: bisect ci.yml startup_failure (matrix+if+permissions)
Agent-Session: claude/reusable-ci-20260616
2026-06-16 11:37:21 +00:00
peptiderepo
84e1bfebfd test: minimal reusable workflow diagnostic
Adds ci-minimal.yml + ci-minimal-test.yml to diagnose the startup_failure
on the self-test reusable workflow call. To be removed after diagnosis.

Agent-Session: claude/reusable-ci-20260616
2026-06-16 11:36:29 +00:00
peptiderepo
e3be036d19 fix(ci-self-test): use relative path for same-repo reusable workflow call
GitHub requires ./.github/workflows/ci.yml (not @branch-ref) for same-repo
reusable workflow calls from a PR branch. The @claude/reusable-ci-20260616
ref caused startup_failure because GitHub can't resolve branch-ref reusable
workflows that aren't on main yet.

Agent-Session: claude/reusable-ci-20260616
2026-06-16 11:32:03 +00:00
peptiderepo
b5e54a4bdd feat: reusable ci.yml — estate CI standard (Phase 0, step 1)
Add .github/workflows/ci.yml as a reusable workflow_call entrypoint for
all 7 estate repos. Consistent job names (lint-php, phpcs, file-size,
phpunit, lint-js) so deploy-app.yml can needs: them uniformly.

Inputs: php_versions (JSON matrix), tests (stubs|wp-framework|brain-monkey|none),
enforce_300, line_limit_paths, has_js, phpcs_autofix.

phpcbf auto-commit: on pull_request, phpcbf runs and commits any auto-fixable
PHPCS violations back to the branch via GITHUB_TOKEN before strict phpcs.
On push events the commit step is skipped to prevent loops.

wp-framework PHPUnit path handles all three known WP CI gotchas:
(1) install svn (ubuntu-latest lacks it);
(2) skip-db-create=true (MySQL service pre-creates wp_tests via MYSQL_DATABASE);
(3) WP_CORE_DIR trailing slash (ABSPATH concatenation guard).

Also adds:
- .github/workflows/ci-self-test.yml: self-test caller (tests: none) so the
  reusable workflow is exercised on this PR.
- README.md: adoption snippet + full input reference for caller repos.
- CHANGELOG.md: v1.6.0 entry.

Agent-Session: claude/reusable-ci-20260616
2026-06-16 11:30:33 +00:00
8 changed files with 9 additions and 411 deletions

View File

@@ -5,7 +5,7 @@
#
# jobs:
# ci:
# uses: peptide-repo/peptide-e2e/.github/workflows/ci.yml@main
# uses: peptiderepo/peptide-e2e/.github/workflows/ci.yml@main
# with:
# tests: stubs # or wp-framework | brain-monkey | none
# has_js: false

View File

@@ -35,7 +35,7 @@ permissions:
jobs:
deploy-staging:
name: Deploy to Staging
runs-on: [self-hosted, peptide-vps]
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
@@ -72,7 +72,7 @@ jobs:
name: Smoke tests (staging)
needs: deploy-staging
if: ${{ inputs.run_smoke }}
uses: peptide-repo/peptide-e2e/.github/workflows/smoke.yml@main
uses: peptiderepo/peptide-e2e/.github/workflows/smoke.yml@main
secrets:
STAGING_WP_ADMIN_USER: ${{ secrets.STAGING_WP_ADMIN_USER }}
STAGING_WP_ADMIN_PASSWORD: ${{ secrets.STAGING_WP_ADMIN_PASSWORD }}
@@ -83,7 +83,7 @@ jobs:
# skipped smoke (run_smoke: false) still allows prod via the if-condition.
needs: [deploy-staging, smoke]
if: ${{ !cancelled() && needs.deploy-staging.result == 'success' && needs.smoke.result != 'failure' }}
runs-on: [self-hosted, peptide-vps]
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4

View File

@@ -22,7 +22,7 @@ jobs:
- uses: actions/github-script@v7
with:
script: |
const owner = 'peptide-repo';
const owner = 'peptiderepo';
const repos = [
'prautoblogger','peptide-repo-core','peptide-search-ai',
'peptide-news-plugin','peptide-reconstitution-calculator','peptide-starter-theme'

View File

@@ -32,14 +32,14 @@ on:
jobs:
smoke:
name: Playwright smoke suite
runs-on: [self-hosted, peptide-vps]
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout peptide-e2e (explicit — workflow_call defaults to caller repo)
uses: actions/checkout@v4
with:
repository: peptide-repo/peptide-e2e
repository: peptiderepo/peptide-e2e
ref: main
- name: Set up Node.js
@@ -53,8 +53,7 @@ jobs:
run: npm ci
- name: Install Playwright browsers
# OS deps pre-installed on the self-hosted runner host; --with-deps needs sudo which the sandbox blocks
run: npx playwright install chromium
run: npx playwright install chromium --with-deps
- name: Run smoke tests
env:

View File

@@ -1,42 +1,5 @@
# Changelog
## [1.7.2] - 2026-06-17
### Fixed
- **CI**: Drop `--with-deps` from `npx playwright install chromium` in `smoke.yml`.
The `peptide-vps` self-hosted runner has OS-level Playwright deps pre-installed by
root; `--with-deps` invokes `apt-get` which requires `sudo`, blocked by the runner's
`NoNewPrivileges=true` systemd hardening. Binary-only install works without
elevated privileges.## [1.7.1] - 2026-06-17
### Changed
- **CI/CD**: Switch `deploy-staging`, `deploy-production`, and `smoke` jobs from
`ubuntu-latest` (GitHub-hosted) to `[self-hosted, peptide-vps]` (org runner on
KVM8 VPS). This routes all Hostinger SSH/rsync traffic through our stable VPS IP,
fixing the Azure-region throttle that caused intermittent deploy failures.
Callers' CI/validate jobs are unaffected (remain on `ubuntu-latest`).
## [1.7.0] - 2026-06-17
### Added
- **Peptide News admin smoke (07)** (`07-peptide-news-admin.spec.ts`).
Covers the Peptide News Plugin admin surfaces: dashboard (HTTP 200, no fatal,
h1 "Peptide News Analytics" heading, `.pn-summary-cards`, `.pn-date-filter`),
settings (form present), articles, and LLM costs sub-pages. Skip-if-absent guard
on all tests using the standard 05/06 merge-early binding pattern. Slugs sourced
from `admin/class-pn-admin-menu.php`.
- **PR Vision admin smoke (08)** (`08-pr-vision-admin.spec.ts`).
Covers the PR Vision plugin admin surfaces: dashboard (HTTP 200, no fatal,
h1 "PR Vision" visible), Settings page (API-key-manager card `#prv-key-card`
present, status badge `#prv-key-source-badge` visible), write-only security
invariant (`#prv_api_key` value="" + full HTML must not contain "sk-or-"),
Costs sub-page (`pr-vision-costs`), and Call Log sub-page (`pr-vision-calls`).
Skip-if-absent at two levels: plugin absent (loud annotation + skip), and
`#prv-key-card` absent (Settings v0.2.0 not yet deployed on environment).
Selectors sourced from `class-prv-admin-page.php`, `class-prv-key-manager-renderer.php`.
All notable changes to the peptide-e2e smoke suite will be documented here.
## [1.6.0] - 2026-06-16

View File

@@ -1,6 +1,6 @@
{
"name": "peptide-e2e",
"version": "1.7.2",
"version": "1.5.0",
"description": "Playwright smoke tests for peptiderepo.com (staging + production)",
"private": true,
"scripts": {

View File

@@ -1,144 +0,0 @@
/**
* 07-peptide-news-admin.spec.ts
*
* What: Smoke tests for the Peptide News Plugin admin surfaces.
* Verifies the dashboard, settings, articles, and cost sub-pages
* load without PHP fatals and checks expected UI elements.
* Guards against activation failures, missing menu registration, or
* PHP fatals introduced by a bad deploy.
*
* Who triggers it: Playwright chromium project (post-auth setup).
* Dependencies: wp-admin authenticated session (from global.setup.ts).
*
* SKIP-IF-ABSENT: When the Peptide News Plugin is not installed/activated on
* this environment, each test detects the absent menu page and skips with a
* loud annotation — consistent with the 05/06 merge-early binding pattern.
*
* Admin menu slugs (sourced from admin/class-pn-admin-menu.php):
* Dashboard -> admin.php?page=peptide-news-dashboard
* Settings -> admin.php?page=peptide-news-settings
* Articles -> admin.php?page=peptide-news-articles
* LLM Costs -> admin.php?page=peptide-news-costs
*
* Selectors sourced from admin/partials/dashboard.php:
* h1 "Peptide News Analytics" - page heading (text match)
* .pn-summary-cards - summary card row
* .pn-date-filter - date range filter form
*
* @see peptiderepo/peptide-news-plugin admin/class-pn-admin-menu.php
* @see peptiderepo/peptide-news-plugin admin/partials/dashboard.php
*/
import { test, expect } from '@playwright/test';
const ADMIN = '/wp-admin';
/**
* Detect the "page not registered" condition WordPress returns when a plugin
* menu slug is unknown. Returns true when the plugin is absent.
*
* WordPress either:
* (a) Shows "You do not have sufficient permissions to access this page."
* (b) Shows a notice that the page cannot be found
* (c) Redirects back to /wp-admin/ stripping the ?page= param
*/
function isPluginAbsent(body: string, url: string): boolean {
return (
body.includes('Invalid plugin page') ||
body.includes('do not have sufficient permissions') ||
body.includes('The page you are looking for') ||
( url.includes('/wp-admin/') && ! url.includes('page=peptide-news') )
);
}
/** Push a loud skip annotation and call test.skip(). */
function skipMissing(): void {
test.info().annotations.push({
type: 'skip',
description:
'[peptide-news smoke] Peptide News Plugin is not installed or activated ' +
'on this environment. Skipping per the merge-early binding.',
});
test.skip();
}
test.describe('WP Admin - Peptide News Plugin dashboard', () => {
test('dashboard page loads (HTTP 200, no PHP fatal)', async ({ page }) => {
const res = await page.goto(`${ADMIN}/admin.php?page=peptide-news-dashboard`);
expect(res?.status()).toBe(200);
await expect(page.locator('#wpbody')).toBeVisible();
const body = (await page.textContent('body')) ?? '';
expect(body).not.toContain('Fatal error');
if (isPluginAbsent(body, page.url())) { skipMissing(); return; }
});
test('dashboard renders analytics heading, summary cards, and date filter', async ({ page }) => {
await page.goto(`${ADMIN}/admin.php?page=peptide-news-dashboard`);
await expect(page.locator('#wpbody')).toBeVisible();
const body = (await page.textContent('body')) ?? '';
expect(body).not.toContain('Fatal error');
if (isPluginAbsent(body, page.url())) { skipMissing(); return; }
// Heading from admin/partials/dashboard.php: <h1>Peptide News Analytics</h1>
await expect(
page.locator('h1').filter({ hasText: 'Peptide News Analytics' })
).toBeVisible({ timeout: 10_000 });
// Summary card row: <div class="pn-summary-cards">
await expect(page.locator('.pn-summary-cards')).toBeVisible({ timeout: 10_000 });
// Date range filter form: <form class="pn-date-filter">
await expect(page.locator('.pn-date-filter')).toBeVisible({ timeout: 10_000 });
});
});
test.describe('WP Admin - Peptide News Plugin settings page', () => {
test('settings page loads (HTTP 200, no fatal, settings form present)', async ({ page }) => {
const res = await page.goto(`${ADMIN}/admin.php?page=peptide-news-settings`);
expect(res?.status()).toBe(200);
await expect(page.locator('#wpbody')).toBeVisible();
const body = (await page.textContent('body')) ?? '';
expect(body).not.toContain('Fatal error');
if (isPluginAbsent(body, page.url())) { skipMissing(); return; }
// Every settings page rendered by class-pn-admin-settings-page.php contains a form
await expect(page.locator('form').first()).toBeVisible({ timeout: 10_000 });
});
});
test.describe('WP Admin - Peptide News Plugin articles page', () => {
test('articles page loads without fatal error', async ({ page }) => {
const res = await page.goto(`${ADMIN}/admin.php?page=peptide-news-articles`);
expect(res?.status()).toBe(200);
await expect(page.locator('#wpbody')).toBeVisible();
const body = (await page.textContent('body')) ?? '';
expect(body).not.toContain('Fatal error');
if (isPluginAbsent(body, page.url())) { skipMissing(); return; }
expect(body).not.toContain('Invalid plugin page');
});
});
test.describe('WP Admin - Peptide News Plugin LLM costs page', () => {
test('costs page loads without fatal error', async ({ page }) => {
const res = await page.goto(`${ADMIN}/admin.php?page=peptide-news-costs`);
expect(res?.status()).toBe(200);
await expect(page.locator('#wpbody')).toBeVisible();
const body = (await page.textContent('body')) ?? '';
expect(body).not.toContain('Fatal error');
if (isPluginAbsent(body, page.url())) { skipMissing(); return; }
expect(body).not.toContain('Invalid plugin page');
});
});

View File

@@ -1,220 +0,0 @@
/**
* 08-pr-vision-admin.spec.ts
*
* What: Smoke tests for the PR Vision plugin admin surfaces.
* Covers the dashboard, Settings (API-key-manager card + write-only
* invariant), Costs, and Call Log sub-pages. Guards against PHP fatals,
* missing menu registration, and key leakage regressions.
*
* Who triggers it: Playwright chromium project (post-auth setup).
* Dependencies: wp-admin authenticated session (from global.setup.ts).
*
* SKIP-IF-ABSENT: When PR Vision is not installed/activated on this
* environment, each test detects the absent page and skips with a loud
* annotation - consistent with the 05/06 merge-early binding pattern.
*
* Admin menu slugs (sourced from includes/core/class-prv-admin-page.php
* and includes/core/class-prv-settings-page.php,
* includes/core/class-prv-costs-page.php,
* includes/core/class-prv-call-log-page.php):
* Dashboard -> admin.php?page=pr-vision (MENU_SLUG = 'pr-vision')
* Settings -> admin.php?page=pr-vision-settings (MENU_SLUG = 'pr-vision-settings')
* Costs -> admin.php?page=pr-vision-costs (MENU_SLUG = 'pr-vision-costs')
* Call Log -> admin.php?page=pr-vision-calls (MENU_SLUG = 'pr-vision-calls')
*
* Selectors sourced from includes/core/class-prv-admin-page.php:
* h1 "PR Vision - AI Visibility" - dashboard page heading
*
* Selectors sourced from includes/core/class-prv-key-manager-renderer.php:
* #prv-key-card - the API-key-manager card container
* #prv-key-source-badge - key source status indicator span
* #prv_api_key - password input (must render empty - write-only invariant)
*
* Write-only invariant (security):
* The stored OpenRouter key must NEVER appear in page source.
* #prv_api_key renders with value="" on every page load regardless of
* whether a key is stored. This spec asserts the empty value and asserts
* that no structurally valid key token (regex /sk-or-v1-[A-Za-z0-9]{20,}/)
* appears in the full body HTML. A regex is used instead of a naive
* toContain('sk-or-') to avoid a false-positive on the password input's
* placeholder="sk-or-…" attribute (the ellipsis is a format hint, not a
* token body, and won't match the regex).
*
* @see peptiderepo/pr-vision includes/core/class-prv-admin-page.php
* @see peptiderepo/pr-vision includes/core/class-prv-settings-page.php
* @see peptiderepo/pr-vision includes/core/class-prv-key-manager-renderer.php
*/
import { test, expect } from '@playwright/test';
const ADMIN = '/wp-admin';
/**
* Detect the "page not registered" condition WordPress returns when a plugin
* menu slug is unknown. Returns true when the plugin is absent.
*/
function isPluginAbsent(body: string, url: string): boolean {
return (
body.includes('Invalid plugin page') ||
body.includes('do not have sufficient permissions') ||
body.includes('The page you are looking for') ||
( url.includes('/wp-admin/') && ! url.includes('page=pr-vision') )
);
}
/** Push a loud skip annotation and call test.skip(). */
function skipMissing(): void {
test.info().annotations.push({
type: 'skip',
description:
'[pr-vision smoke] PR Vision plugin is not installed or activated ' +
'on this environment. Skipping per the merge-early binding.',
});
test.skip();
}
test.describe('WP Admin - PR Vision dashboard', () => {
test('dashboard page loads (HTTP 200, no PHP fatal)', async ({ page }) => {
const res = await page.goto(`${ADMIN}/admin.php?page=pr-vision`);
expect(res?.status()).toBe(200);
await expect(page.locator('#wpbody')).toBeVisible();
const body = (await page.textContent('body')) ?? '';
expect(body).not.toContain('Fatal error');
if (isPluginAbsent(body, page.url())) { skipMissing(); return; }
// Heading from class-prv-admin-page.php render_page():
// echo '<h1>' . esc_html__( 'PR Vision - AI Visibility', 'pr-vision' ) . '</h1>';
await expect(
page.locator('h1').filter({ hasText: 'PR Vision' })
).toBeVisible({ timeout: 10_000 });
});
});
test.describe('WP Admin - PR Vision Settings page (API-key-manager)', () => {
test('settings page loads (HTTP 200, no PHP fatal)', async ({ page }) => {
const res = await page.goto(`${ADMIN}/admin.php?page=pr-vision-settings`);
expect(res?.status()).toBe(200);
await expect(page.locator('#wpbody')).toBeVisible();
const body = (await page.textContent('body')) ?? '';
expect(body).not.toContain('Fatal error');
if (isPluginAbsent(body, page.url())) { skipMissing(); return; }
});
test('API-key-manager card is present and status indicator visible', async ({ page }) => {
await page.goto(`${ADMIN}/admin.php?page=pr-vision-settings`);
await expect(page.locator('#wpbody')).toBeVisible();
const body = (await page.textContent('body')) ?? '';
expect(body).not.toContain('Fatal error');
if (isPluginAbsent(body, page.url())) { skipMissing(); return; }
// ── SKIP-IF-ABSENT guard: Settings page may not have key-card if Settings
// subpage (v0.2.0) is not yet deployed on this environment.
const keyCard = page.locator('#prv-key-card');
const cardPresent = await keyCard.count() > 0;
if ( ! cardPresent ) {
test.info().annotations.push({
type: 'skip',
description:
'[pr-vision smoke] #prv-key-card absent: PR Vision Settings page ' +
'(v0.2.0 admin) is not deployed on this environment. Skipping key-manager assertions.',
});
test.skip();
return;
}
// API-key-manager card container from class-prv-key-manager-renderer.php:
// <div class="prv-card" id="prv-key-card">
await expect(keyCard).toBeVisible({ timeout: 10_000 });
// Key source status indicator: <span ... id="prv-key-source-badge">
// Shows one of: "Set via wp-config", "Set via admin", "Not set"
await expect(page.locator('#prv-key-source-badge')).toBeVisible({ timeout: 10_000 });
});
test('password input is present and renders EMPTY (write-only invariant)', async ({ page }) => {
await page.goto(`${ADMIN}/admin.php?page=pr-vision-settings`);
await expect(page.locator('#wpbody')).toBeVisible();
const body = (await page.textContent('body')) ?? '';
expect(body).not.toContain('Fatal error');
if (isPluginAbsent(body, page.url())) { skipMissing(); return; }
const keyCard = page.locator('#prv-key-card');
const cardPresent = await keyCard.count() > 0;
if ( ! cardPresent ) {
test.info().annotations.push({
type: 'skip',
description:
'[pr-vision smoke] #prv-key-card absent: skipping write-only invariant check.',
});
test.skip();
return;
}
// Password input from class-prv-key-manager-renderer.php render_key_form():
// <input type="password" id="prv_api_key" name="prv_api_key" ... value="" ...>
// The renderer explicitly passes value="" -- the stored key is NEVER output.
const keyInput = page.locator('#prv_api_key');
// When the key source is SOURCE_CONSTANT the input is disabled but still rendered.
// In all cases it must be present in the DOM.
await expect(keyInput).toBeAttached({ timeout: 10_000 });
// Write-only invariant: value attribute must be empty string, not the stored key.
const inputValue = await keyInput.getAttribute('value');
expect(inputValue ?? '').toBe('');
// Security check: a *real* OpenRouter key (format "sk-or-v1-<20+ alnum chars>")
// must not appear anywhere in the page HTML -- guards against accidental
// server-side leakage.
//
// NOTE: We use a regex rather than the naive toContain('sk-or-') to avoid
// a false-positive: the password input carries placeholder="sk-or-…"
// (an ellipsis format-hint, not a token body) which contains the literal
// string "sk-or-" but is NOT a leaked key. A regex that requires the
// "v1-" segment and at least 20 alphanumeric characters only matches a
// structurally valid key, so the placeholder never trips it.
const html = await page.content();
expect(html).not.toMatch(/sk-or-v1-[A-Za-z0-9]{20,}/);
});
});
test.describe('WP Admin - PR Vision Costs page', () => {
test('costs page loads without fatal error', async ({ page }) => {
const res = await page.goto(`${ADMIN}/admin.php?page=pr-vision-costs`);
expect(res?.status()).toBe(200);
await expect(page.locator('#wpbody')).toBeVisible();
const body = (await page.textContent('body')) ?? '';
expect(body).not.toContain('Fatal error');
if (isPluginAbsent(body, page.url())) { skipMissing(); return; }
expect(body).not.toContain('Invalid plugin page');
});
});
test.describe('WP Admin - PR Vision Call Log page', () => {
test('call log page loads without fatal error', async ({ page }) => {
const res = await page.goto(`${ADMIN}/admin.php?page=pr-vision-calls`);
expect(res?.status()).toBe(200);
await expect(page.locator('#wpbody')).toBeVisible();
const body = (await page.textContent('body')) ?? '';
expect(body).not.toContain('Fatal error');
if (isPluginAbsent(body, page.url())) { skipMissing(); return; }
expect(body).not.toContain('Invalid plugin page');
});
});