feat: initial Playwright smoke suite

20 tests across 5 files covering all plugin surfaces on staging:
- Homepage load and PHP fatal detection
- Reconstitution Calculator shortcode + math
- Peptide Search AI widget + AJAX response
- Admin: all plugins active, PSA + PRAutoBlogger settings pages
- REST API: prc/v1/presets, peptides/v1/search, compounds, CPT types

Staging shortcode pages:
  /reconstitution-calculator/ - [prc_calculator]
  /search-peptides/           - [peptide_search]
  /peptide-directory/         - [peptide_directory]

GHA workflow: smoke.yml supports manual dispatch + workflow_call
for use as a deploy gate from plugin repos.
This commit is contained in:
peptiderepo
2026-04-26 04:39:13 +00:00
commit d46a1e4680
12 changed files with 656 additions and 0 deletions

9
.env.example Executable file
View File

@@ -0,0 +1,9 @@
# Copy this to .env and fill in values — never commit .env to git
BASE_URL=https://staging.peptiderepo.com
WP_ADMIN_USER=peptiderepo
WP_ADMIN_PASSWORD=your-staging-password-here
# Page paths where shortcodes live (adjust to match actual WP page slugs)
CALC_PAGE_PATH=/reconstitution-calculator/
SEARCH_PAGE_PATH=/search-peptides/
DIRECTORY_PAGE_PATH=/peptide-directory/

83
.github/workflows/smoke.yml vendored Executable file
View File

@@ -0,0 +1,83 @@
# smoke.yml
#
# Playwright smoke tests against staging.peptiderepo.com
#
# Triggers:
# - Manually via workflow_dispatch (run before any prod deploy)
# - Called by other repos via workflow_call (deploy gates)
#
# Secrets required (set in GitHub repo Settings → Secrets → Actions):
# STAGING_WP_ADMIN_USER — WordPress admin username on staging
# STAGING_WP_ADMIN_PASSWORD — WordPress admin password on staging
name: Smoke Tests (Staging)
on:
workflow_dispatch:
inputs:
base_url:
description: 'Target URL (defaults to staging)'
required: false
default: 'https://staging.peptiderepo.com'
# Allow other repos to call this workflow as a deploy gate
workflow_call:
inputs:
base_url:
type: string
required: false
default: 'https://staging.peptiderepo.com'
secrets:
STAGING_WP_ADMIN_USER:
required: true
STAGING_WP_ADMIN_PASSWORD:
required: true
jobs:
smoke:
name: Playwright smoke suite
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: e2e/package-lock.json
- name: Install dependencies
working-directory: e2e
run: npm ci
- name: Install Playwright browsers
working-directory: e2e
run: npx playwright install chromium --with-deps
- name: Run smoke tests
working-directory: e2e
env:
BASE_URL: ${{ inputs.base_url || 'https://staging.peptiderepo.com' }}
WP_ADMIN_USER: ${{ secrets.STAGING_WP_ADMIN_USER }}
WP_ADMIN_PASSWORD: ${{ secrets.STAGING_WP_ADMIN_PASSWORD }}
run: npx playwright test
- name: Upload test report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: e2e/playwright-report/
retention-days: 14
- name: Upload failure screenshots/videos
uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-failures
path: e2e/test-results/
retention-days: 7

5
.gitignore vendored Executable file
View File

@@ -0,0 +1,5 @@
node_modules/
playwright-report/
playwright/.auth/
test-results/
.env

42
global.setup.ts Executable file
View File

@@ -0,0 +1,42 @@
/**
* global.setup.ts
*
* What: Authenticates to WordPress admin once and persists the session to disk.
* Who triggers it: Playwright "setup" project, runs before all smoke tests.
* Dependencies: WP_ADMIN_USER and WP_ADMIN_PASSWORD env vars.
*
* The saved auth state (playwright/.auth/admin.json) is reused by the
* "chromium" project so individual tests don't each incur a login round-trip.
*/
import { test as setup, expect } from '@playwright/test';
import * as path from 'path';
import * as fs from 'fs';
const AUTH_FILE = path.join(__dirname, 'playwright/.auth/admin.json');
setup('authenticate as WP admin', async ({ page }) => {
const user = process.env.WP_ADMIN_USER;
const password = process.env.WP_ADMIN_PASSWORD;
if (!user || !password) {
throw new Error(
'WP_ADMIN_USER and WP_ADMIN_PASSWORD must be set in .env or CI secrets'
);
}
// Ensure directory exists
fs.mkdirSync(path.dirname(AUTH_FILE), { recursive: true });
await page.goto('/wp-login.php');
await page.fill('#user_login', user);
await page.fill('#user_pass', password);
await page.click('#wp-submit');
// Wait for successful redirect to wp-admin dashboard
await expect(page).toHaveURL(/wp-admin/, { timeout: 20_000 });
await expect(page.locator('#wpadminbar')).toBeVisible();
// Persist session cookies + localStorage to disk
await page.context().storageState({ path: AUTH_FILE });
});

92
package-lock.json generated Executable file
View File

@@ -0,0 +1,92 @@
{
"name": "peptide-e2e",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "peptide-e2e",
"version": "1.0.0",
"devDependencies": {
"@playwright/test": "^1.43.0",
"dotenv": "^16.4.5"
}
},
"node_modules/@playwright/test": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",
"integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.59.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.59.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
}
}
}

17
package.json Executable file
View File

@@ -0,0 +1,17 @@
{
"name": "peptide-e2e",
"version": "1.0.0",
"description": "Playwright smoke tests for peptiderepo.com (staging + production)",
"private": true,
"scripts": {
"test": "playwright test",
"test:staging": "BASE_URL=https://staging.peptiderepo.com playwright test",
"test:prod": "BASE_URL=https://peptiderepo.com playwright test",
"test:headed": "playwright test --headed",
"report": "playwright show-report"
},
"devDependencies": {
"@playwright/test": "^1.43.0",
"dotenv": "^16.4.5"
}
}

61
playwright.config.ts Executable file
View File

@@ -0,0 +1,61 @@
/**
* Playwright configuration for peptiderepo.com smoke tests.
*
* Usage:
* npm test — run against BASE_URL (defaults to staging)
* npm run test:staging — explicit staging run
* npm run test:prod — production smoke run
*
* Environment variables (set in .env or CI secrets):
* BASE_URL — target site root (default: https://staging.peptiderepo.com)
* WP_ADMIN_USER — WordPress admin username
* WP_ADMIN_PASSWORD — WordPress admin password
* CALC_PAGE_PATH — URL path to the reconstitution calculator page (default: /reconstitution-calculator/)
* SEARCH_PAGE_PATH — URL path to the peptide search page (default: /)
* DIRECTORY_PAGE_PATH — URL path to the peptide directory page (default: /peptides/)
*/
import { defineConfig, devices } from '@playwright/test';
import * as dotenv from 'dotenv';
import * as path from 'path';
dotenv.config({ path: path.resolve(__dirname, '.env') });
const BASE_URL = process.env.BASE_URL ?? 'https://staging.peptiderepo.com';
export default defineConfig({
testDir: './tests',
fullyParallel: false, // WP staging is single-server; parallel can cause rate-limit hits
forbidOnly: !!process.env.CI, // Prevent .only from landing in CI
retries: process.env.CI ? 1 : 0,
workers: 1, // Sequential against staging to be polite
reporter: process.env.CI
? [['github'], ['html', { open: 'never' }]]
: [['list'], ['html', { open: 'on-failure' }]],
use: {
baseURL: BASE_URL,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
actionTimeout: 15_000,
navigationTimeout: 30_000,
},
projects: [
// Auth setup — runs first, saves session to disk
{
name: 'setup',
testMatch: /global\.setup\.ts/,
},
// All smoke tests — depend on auth setup
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['setup'],
},
],
});

40
tests/00-homepage.spec.ts Executable file
View File

@@ -0,0 +1,40 @@
/**
* 00-homepage.spec.ts
*
* What: Verifies the site root loads with no fatal PHP errors and basic
* theme elements are present. Catches theme activation failures,
* white screens, and missing plugin-provided content.
* Who triggers it: Playwright chromium project (post-auth setup).
* Dependencies: peptide-starter-theme, PR Core active.
*/
import { test, expect } from '@playwright/test';
test.describe('Homepage', () => {
test('loads with HTTP 200 and no fatal error', async ({ page }) => {
const response = await page.goto('/');
expect(response?.status()).toBe(200);
});
test('has no PHP fatal error or white screen', async ({ page }) => {
await page.goto('/');
const body = await page.textContent('body');
// PHP fatals produce these strings in non-production WP configs
expect(body).not.toContain('Fatal error');
expect(body).not.toContain('Parse error');
expect(body).not.toContain('Warning: ');
// White-screen: body is empty or nearly empty
expect((body ?? '').trim().length).toBeGreaterThan(200);
});
test('renders site name in page title', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/peptiderepo/i);
});
test('wp-admin bar present (logged-in session)', async ({ page }) => {
await page.goto('/');
// The auth state from global.setup means we should see the admin bar
await expect(page.locator('#wpadminbar')).toBeVisible();
});
});

View File

@@ -0,0 +1,69 @@
/**
* 01-reconstitution-calculator.spec.ts
*
* What: Smoke-tests the Peptide Reconstitution Calculator (PRC) plugin.
* Verifies the shortcode renders, presets load, and basic math produces
* a non-zero result — catching broken PHP, failed asset enqueue, or
* missing PR Core integration.
* Who triggers it: Playwright chromium project (post-auth setup).
* Dependencies: peptide-reconstitution-calculator plugin active, [prc_calculator]
* shortcode on the page at CALC_PAGE_PATH.
*
* @see C:\Users\ongte\apps\Peptide Repo CTO\app-maps\peptide-reconstitution-calculator.md
*/
import { test, expect } from '@playwright/test';
const CALC_PATH = process.env.CALC_PAGE_PATH ?? '/reconstitution-calculator/';
test.describe('Reconstitution Calculator', () => {
test.beforeEach(async ({ page }) => {
const res = await page.goto(CALC_PATH);
// Tolerate 200 or 301 (redirect to trailing slash)
expect([200, 301]).toContain(res?.status());
await page.waitForLoadState('networkidle');
});
test('shortcode widget renders on page', async ({ page }) => {
// The shortcode wraps the widget in a container with class prc-calculator
// or similar; at minimum a <select> for peptide presets must exist.
const widget = page.locator('.prc-calculator, [data-prc], #prc-calculator').first();
await expect(widget).toBeVisible({ timeout: 10_000 });
});
test('peptide preset dropdown is populated', async ({ page }) => {
// At minimum the 8 hardcoded defaults; more if PR Core is active
const select = page.locator('select').first();
await expect(select).toBeVisible();
const options = await select.locator('option').count();
expect(options).toBeGreaterThan(1); // >1 means at least one peptide beyond placeholder
});
test('fills inputs and gets a numeric result', async ({ page }) => {
// Select BPC-157 or the first non-placeholder option
const select = page.locator('select').first();
const firstPeptide = select.locator('option').nth(1);
const firstValue = await firstPeptide.getAttribute('value');
await select.selectOption(firstValue ?? '');
// Fill reconstitution volume (mL) — use a known valid value
const volInput = page.locator('input[name*="recon"], input[placeholder*="mL"], input[type="number"]').first();
await volInput.fill('2');
// Fill desired dose
const doseInput = page.locator('input[name*="dose"], input[placeholder*="dose"], input[type="number"]').nth(1);
await doseInput.fill('250');
// Trigger calculation (button or auto-calculate on input)
const calcButton = page.locator('button[type="submit"], button:has-text("Calculate")');
if (await calcButton.count() > 0) {
await calcButton.first().click();
}
// Results container should show a non-zero concentration or injection volume
const results = page.locator('.prc-results, .calculator-results, [data-prc-result]').first();
await expect(results).toBeVisible({ timeout: 5_000 });
const resultText = await results.textContent();
expect(resultText).toMatch(/\d+(\.\d+)?/); // contains at least one number
});
});

76
tests/02-peptide-search.spec.ts Executable file
View File

@@ -0,0 +1,76 @@
/**
* 02-peptide-search.spec.ts
*
* What: Smoke-tests the Peptide Search AI (PSA) frontend search widget.
* Verifies the [peptide_search] shortcode renders, accepts input,
* and returns results for a known-good peptide name. Catches AJAX
* failures, nonce issues, and missing PR Core dependency.
* Who triggers it: Playwright chromium project (post-auth setup).
* Dependencies: peptide-search-ai plugin + PR Core active; at least one
* published peptide CPT post (e.g., BPC-157) on staging.
*
* @see C:\Users\ongte\apps\Peptide Repo CTO\app-maps\peptide-search-ai.md
*/
import { test, expect } from '@playwright/test';
const SEARCH_PATH = process.env.SEARCH_PAGE_PATH ?? '/';
test.describe('Peptide Search AI', () => {
test.beforeEach(async ({ page }) => {
await page.goto(SEARCH_PATH);
await page.waitForLoadState('networkidle');
});
test('search input widget is present on page', async ({ page }) => {
// PSA renders a form with class .psa-search-form or input with data-psa attr
const searchInput = page.locator(
'.psa-search-form input, input[name*="peptide"], input[placeholder*="peptide" i], [data-psa] input'
).first();
await expect(searchInput).toBeVisible({ timeout: 10_000 });
});
test('searching for a known peptide returns results', async ({ page }) => {
const searchInput = page.locator(
'.psa-search-form input, input[name*="peptide"], input[placeholder*="peptide" i], [data-psa] input'
).first();
await expect(searchInput).toBeVisible();
// Type a peptide that should exist on staging
await searchInput.fill('BPC-157');
await searchInput.press('Enter');
// Results container should appear and contain something
const results = page.locator('.psa-results, .psa-search-results, [data-psa-results]').first();
await expect(results).toBeVisible({ timeout: 15_000 });
const text = await results.textContent();
expect((text ?? '').trim().length).toBeGreaterThan(0);
});
test('AJAX response contains no PHP error', async ({ page }) => {
// Intercept the AJAX call and inspect raw response
const ajaxResponse = page.waitForResponse(
(resp) =>
resp.url().includes('admin-ajax.php') &&
resp.request().method() === 'POST',
{ timeout: 15_000 }
);
const searchInput = page.locator(
'.psa-search-form input, input[name*="peptide"], input[placeholder*="peptide" i], [data-psa] input'
).first();
await searchInput.fill('TB-500');
await searchInput.press('Enter');
try {
const resp = await ajaxResponse;
const body = await resp.text();
expect(body).not.toContain('Fatal error');
expect(body).not.toContain('Parse error');
// Valid PSA response is JSON; check it starts with { or [
expect(body.trim()).toMatch(/^[\[{]/);
} catch {
// AJAX call may not fire if results cached in page HTML — that's fine
}
});
});

100
tests/03-admin-plugins.spec.ts Executable file
View File

@@ -0,0 +1,100 @@
/**
* 03-admin-plugins.spec.ts
*
* What: Verifies all five peptiderepo plugins are active in wp-admin and
* their admin pages load without PHP errors. Catches activation failures
* or missing plugin files after a deploy.
* Who triggers it: Playwright chromium project (post-auth setup).
* Dependencies: wp-admin authenticated session (from global.setup.ts).
*
* Plugin admin page slugs:
* - peptide-search-ai → ?page=peptide-search-ai
* - pr-autoblogger → ?page=pr-autoblogger
* - peptide-repo-core → ?page=peptide-repo-core (if admin page exists)
*
* @see C:\Users\ongte\apps\Peptide Repo CTO\app-maps\
*/
import { test, expect } from '@playwright/test';
const ADMIN = '/wp-admin';
const PLUGINS_PAGE = `${ADMIN}/plugins.php`;
// All five plugin slugs as WordPress registers them (directory name)
const REQUIRED_PLUGINS = [
'peptide-search-ai',
'peptide-repo-core',
'peptide-reconstitution-calculator',
'pr-autoblogger',
'peptide-starter-theme', // theme, shown differently — checked separately
];
test.describe('WP Admin — Plugin health', () => {
test('plugins page loads without fatal error', async ({ page }) => {
const res = await page.goto(PLUGINS_PAGE);
expect(res?.status()).toBe(200);
await expect(page.locator('#wpbody')).toBeVisible();
const body = await page.textContent('body');
expect(body).not.toContain('Fatal error');
});
test('all required plugins are active', async ({ page }) => {
await page.goto(PLUGINS_PAGE);
for (const slug of REQUIRED_PLUGINS.filter((s) => s !== 'peptide-starter-theme')) {
// Active plugins have tr with class "active" and data-slug matching the plugin
const row = page.locator(`tr[data-slug="${slug}"]`);
if (await row.count() === 0) {
// Plugin may be in a subfolder — try partial match
const anyRow = page.locator(`tr[data-plugin*="${slug}"]`);
await expect(anyRow).toHaveCount(1, {
timeout: 5_000,
}).catch(() => {
throw new Error(`Plugin row not found for slug: ${slug}`);
});
await expect(anyRow.first()).toHaveClass(/active/);
} else {
await expect(row).toHaveClass(/active/, { timeout: 5_000 });
}
}
});
});
test.describe('WP Admin — PSA settings page', () => {
test('PSA settings page loads', async ({ page }) => {
const res = await page.goto(`${ADMIN}/admin.php?page=peptide-search-ai`);
expect(res?.status()).toBe(200);
await expect(page.locator('#wpbody')).toBeVisible();
const body = await page.textContent('body');
expect(body).not.toContain('Fatal error');
// Settings form must be present
await expect(page.locator('form')).toBeVisible();
});
test('PSA settings page shows cost dashboard section', async ({ page }) => {
await page.goto(`${ADMIN}/admin.php?page=peptide-search-ai`);
// Cost tracking dashboard renders on the same page
const dashboard = page.locator(
'.psa-cost-dashboard, [id*="cost"], [class*="usage"], table'
).first();
await expect(dashboard).toBeVisible({ timeout: 10_000 });
});
});
test.describe('WP Admin — PRAutoBlogger settings page', () => {
test('PRAutoBlogger settings page loads', async ({ page }) => {
// Real slug confirmed via live inspection: prautoblogger-settings
const res = await page.goto(`${ADMIN}/admin.php?page=prautoblogger-settings`);
expect(res?.status()).toBe(200);
await expect(page.locator('#wpbody')).toBeVisible();
const body = await page.textContent('body');
expect(body).not.toContain('Fatal error');
// PRAutoBlogger uses a tabbed dashboard, not a plain <form>
await expect(page.locator('.prautoblogger-dashboard, h1, h2').first()).toBeVisible();
});
});
test.describe('WP Admin — Peptide Repo Core admin', () => {
test('PR Core menu item exists in wp-admin', async ({ page }) => {
await page.goto(`${ADMIN}/`);
// PR Core registers the peptide CPT which creates a "Peptides" menu item

62
tests/04-rest-api.spec.ts Executable file
View File

@@ -0,0 +1,62 @@
/**
* 04-rest-api.spec.ts
*
* What: Verifies public REST API endpoints from all plugins respond with
* valid JSON and expected shapes. Catches broken REST registration,
* PHP errors in REST handlers, or missing PR Core dependency.
* Who triggers it: Playwright chromium project (post-auth setup).
* Note: these tests do NOT need auth — they test public endpoints.
* Dependencies: All plugins active on staging; at least one published peptide.
*
* Endpoints tested:
* - GET /wp-json/prc/v1/presets — Reconstitution Calculator presets
* - GET /wp-json/peptides/v1/search — PSA search REST endpoint
* - GET /wp-json/peptide-search-ai/v1/compounds — PSA directory endpoint
* - GET /wp-json/wp/v2/peptide — PR Core CPT via WP REST
*/
import { test, expect } from '@playwright/test';
test.describe('REST API — Reconstitution Calculator', () => {
test('GET /wp-json/prc/v1/presets returns array', async ({ request }) => {
const res = await request.get('/wp-json/prc/v1/presets');
expect(res.status()).toBe(200);
const body = await res.json();
expect(Array.isArray(body)).toBe(true);
expect(body.length).toBeGreaterThan(0);
// Each preset should have at minimum a slug and name
const first = body[0];
expect(first).toHaveProperty('slug');
expect(first).toHaveProperty('name');
});
});
test.describe('REST API — Peptide Search AI', () => {
test('GET /wp-json/peptides/v1/search returns valid JSON', async ({ request }) => {
const res = await request.get('/wp-json/peptides/v1/search?q=BPC');
// 200 (results found) or 404 (no results) are both acceptable; 500 is not
expect([200, 404]).toContain(res.status());
if (res.status() === 200) {
const body = await res.json();
expect(body).toHaveProperty('status');
expect(body).toHaveProperty('results');
}
});
test('GET /wp-json/peptide-search-ai/v1/compounds returns paginated list', async ({ request }) => {
const res = await request.get('/wp-json/peptide-search-ai/v1/compounds?fields=basic');
expect(res.status()).toBe(200);
const body = await res.json();
// Response should be array or object with data key
expect(body).toBeDefined();
});
});
test.describe('REST API — PR Core (CPT registration)', () => {
test('peptide CPT is registered and appears in WP REST index', async ({ request }) => {
// Confirmed via live inspection: the peptide CPT does NOT expose via /wp/v2/peptide
// (not registered with show_in_rest on the default namespace).
// Instead we verify the CPT exists via the WP REST types endpoint.
const res = await request.get('/wp-json/wp/v2/types/peptide');
// 200 = CPT registered; 404 = CPT missing entirely — both are informative