Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ pnpm-debug.log*
*.sqlite-journal
*.tsbuildinfo
*.swp
.siteos/pulse/
52 changes: 52 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,55 @@ overlapping runs and alert on a non-zero exit code. Each runner writes one JSON
one safe error message to stderr; credentials and login tokens are never emitted.

Website confirmation links require `DENSIO_SYNTHETIC_WEBSITE_URL` alongside `DENSIO_SYNTHETIC_API_URL` for opt-in deployed synthetics. The harness validates the email origin and submits confirmation JSON to the configured API; it never follows an arbitrary email destination.

## SiteOS Pulse monitoring

The `pulse/*.spec.ts` checks monitor public journeys on `https://densio.sh` and the agent API at
`https://api.densio.sh`. The root `siteos.config.json` owns their schedules and
`siteos.playwright.config.ts` owns browser settings. These production checks run only through
explicit Pulse or Playwright commands; Vitest continues to discover `*.test.ts` only.
The existing `pnpm typecheck:e2e` and root `pnpm check` include their static checks.

| Check | Schedule | Coverage |
| --------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| Agent onboarding | Every 15 minutes | Homepage, copied installation prompt, Get Started, and return home. |
| Sign-in and account entry | Every 15 minutes | Invalid-email validation without writes, anonymous account redirects, safe return destinations, and canceled-checkout navigation. |
| Agent API and runtime skill | Every 15 minutes | Media-tool readiness and anonymous runtime-skill retrieval with version and checksum metadata. |
| Mobile sign-in journey | Hourly | Homepage viewport fit, menu navigation, and usable sign-in form at 390 × 844. |
| Legal page navigation | Daily | Footer links reach readable privacy and terms pages. |

The five checks contain 11 tests and total 313 scheduled check runs per day when active. This is
a run count, not a billing estimate. They require no credentials or stateful production actions;
email delivery, authenticated processing, storage, and payments remain outside this coverage.
Legal-page checks verify navigation and rendering; the current content contains placeholders.
Authoring and production rules live in [pulse/AGENTS.md](pulse/AGENTS.md).

Run these commands from the repository root, using Node.js 22.18+ and pnpm 11.7.0 through Corepack:

```sh
corepack pnpm install --frozen-lockfile
corepack pnpm exec playwright install chromium
corepack pnpm typecheck:e2e
SITEOS_AUTH_BASE_URL=https://app.siteos.sh npx @siteoshq/cli project use PROJECT_ID --organization ORGANIZATION_ID --environment production --json
SITEOS_AUTH_BASE_URL=https://app.siteos.sh npx @siteoshq/cli project status --json
SITEOS_AUTH_BASE_URL=https://app.siteos.sh npx @siteoshq/cli pulse validate --json
SITEOS_AUTH_BASE_URL=https://app.siteos.sh npx @siteoshq/cli pulse sync --check --json
SITEOS_AUTH_BASE_URL=https://app.siteos.sh npx @siteoshq/cli pulse test
SITEOS_AUTH_BASE_URL=https://app.siteos.sh npx @siteoshq/cli pulse deploy --dry-run --json
```

Use the exact IDs from the approved task or SiteOS. Confirm the selected Densio production URL
and Pulse attachment in the safe status output. To run one spec through the pinned local runner:

```sh
PLAYWRIGHT_BASE_URL=https://densio.sh corepack pnpm exec playwright test --config siteos.playwright.config.ts e2e/pulse/onboarding.spec.ts
```

The API spec always targets `https://api.densio.sh`, including when the website base URL is
overridden. Run only the intended browser specs for local website checks.

After explicit publication approval, use `pulse deploy --json`. CLI 2.13.0+ supports
`pulse run --check CHECK_ID --request-id REQUEST_ID --json`; take Check IDs from the deployment
result and read terminal Run results through SiteOS MCP. Scheduling and remote health remain
unverified until deployment and subsequent scheduled results; a local pass or dry run does not
establish either. Local failure artifacts and deployment archives are ignored under `.siteos/pulse/`.
2 changes: 1 addition & 1 deletion e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"synthetic:production": "tsx deployed/production.ts",
"synthetic:staging": "tsx deployed/staging.ts",
"test": "vitest run --config vitest.config.ts",
"typecheck": "tsgo --noEmit --project tsconfig.json --pretty false",
"typecheck": "tsgo --noEmit --project tsconfig.json --pretty false && tsgo --noEmit --project pulse/tsconfig.json --pretty false",
"test:fast": "vitest run --config vitest.config.ts --exclude golden-path.test.ts",
"test:media": "vitest run --config vitest.config.ts golden-path.test.ts"
},
Expand Down
41 changes: 41 additions & 0 deletions e2e/pulse/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Pulse monitoring checks

## Ownership and execution

- Keep public website and agent-service monitors here as `*.spec.ts`. The root
`siteos.config.json` selects checks and schedules; `siteos.playwright.config.ts` owns Chromium
configuration. Update their paths together when moving a spec.
- Preserve the existing `e2e` Vitest `**/*.test.ts` discovery. Production Pulse requests must not
run through `pnpm test` or `pnpm check`. The existing `e2e` typecheck includes these specs and
the root Playwright configuration.
- Keep browser DOM types in this directory's `tsconfig.json`, separate from the parent Node-only
harness. The `e2e` package's `typecheck` command must run both configurations; combining their
global types causes incompatible Node and browser stream definitions in the API harness.
- Keep coverage descriptions and operator commands in `../README.md#siteos-pulse-monitoring`;
keep durable agent rules here. Do not duplicate the SiteOS plugin's general workflow.

## Production boundaries

- Use fresh anonymous browser contexts and relative website paths. The selected SiteOS environment
supplies `PLAYWRIGHT_BASE_URL`. Keep a single worker, no retries, bounded timeouts, and failure
artifacts under the Git-ignored `.siteos/pulse/` directory.
- `agent-service.spec.ts` deliberately targets the public CLI's production default,
`https://api.densio.sh`. A website base-URL override does not retarget it. Exclude that spec when
checking only a local website; do not infer another API origin from a website hostname.
- Do not send login emails, create or change accounts, upload media, start processing jobs, or
perform purchases. Preserve the invalid-email check's write-request blocking and its assertion
that no write was attempted. New stateful scenarios need an explicitly authorized isolated
test-data contract; see the deployed synthetics in the parent README.
- Keep Project IDs and credentials in private CLI state. Never put tokens, auth state, signed
artifact URLs, or private data in the bundle or tracked reports.

## Verification and publication

- For changes to these checks, run the existing `pnpm typecheck:e2e`, then the Pulse validation,
sync preview, test, and deployment dry-run commands documented in the parent README. Apply the
repository's completion checks as well. Inspect the manifest for only intended runtime files.
- Preserve meaningful user-observable assertions. API readiness does not prove a completed
compression job; legal-page rendering does not establish the quality of legal content.
- Publication requires the user's explicit approval. After an approved deployment, inspect
terminal remote results for each deployed check. Report local tests, bundle construction,
deployment, manual runs, and subsequent scheduled results separately.
35 changes: 35 additions & 0 deletions e2e/pulse/agent-service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { expect, test } from "@playwright/test";

// This check intentionally targets the production API used by the public Densio CLI.
const apiUrl = "https://api.densio.sh";

test("The agent API and media tools are ready", async ({ request }) => {
const response = await request.get(`${apiUrl}/ready`, { timeout: 15_000 });
expect(response.status()).toBe(200);
const payload: unknown = await response.json();
expect(payload).toMatchObject({
status: "ready",
ffmpegVersion: expect.stringMatching(/\S+/),
ffprobeVersion: expect.stringMatching(/\S+/),
});
});

test("An anonymous agent can retrieve the current runtime skill", async ({ request }) => {
const response = await request.get(`${apiUrl}/v1/skill`, { timeout: 15_000 });
expect(response.status()).toBe(200);
const payload: unknown = await response.json();
expect(payload).toMatchObject({
ok: true,
data: {
entrypoint: "SKILL.md",
skillVersion: expect.stringMatching(/^sha256:[a-f0-9]{64}$/),
files: expect.arrayContaining([
expect.objectContaining({
path: "SKILL.md",
content: expect.stringContaining("densio"),
sha256: expect.stringMatching(/^[a-f0-9]{64}$/),
}),
]),
},
});
});
52 changes: 52 additions & 0 deletions e2e/pulse/authentication.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { expect, test } from "@playwright/test";

test("Email sign-in rejects empty and malformed input without sending a request", async ({
page,
}) => {
const blockedWrites: string[] = [];
// Keep this production monitor safe even if native form validation regresses.
await page.route("**/*", async (route) => {
const method = route.request().method();
if (["GET", "HEAD", "OPTIONS"].includes(method)) return route.continue();
blockedWrites.push(method);
await route.abort("blockedbyclient");
});
const response = await page.goto("/auth/login");
expect(response?.status()).toBe(200);
const email = page.getByRole("textbox", { name: "Email", exact: true });
const submit = page.getByRole("button", { name: "Continue with email" });
await expect(email).toHaveAttribute("type", "email");
await expect(email).toHaveAttribute("required", "");
await submit.click();
await expect(email).toBeFocused();
expect(await email.evaluate((input: HTMLInputElement) => input.validity.valueMissing)).toBe(true);
await email.fill("invalid-email");
await submit.click();
await expect(email).toBeFocused();
expect(await email.evaluate((input: HTMLInputElement) => input.validity.typeMismatch)).toBe(true);
await expect(page.getByRole("heading", { name: "Log in to Densio" })).toBeVisible();
await expect(page.getByRole("heading", { name: "Check your email" })).toHaveCount(0);
expect(blockedWrites).toEqual([]);
});

test("An anonymous account visit preserves the sign-in return destination", async ({ page }) => {
await page.goto("/app");
await expect(page).toHaveURL(/\/auth\/login\?returnTo=%2Fapp$/);
await expect(page.getByRole("heading", { name: "Log in to Densio" })).toBeVisible();
await expect(page.locator('input[name="returnTo"]')).toHaveValue("/app");
});

test("Login ignores an external return destination", async ({ page }) => {
await page.goto("/auth/login?returnTo=https%3A%2F%2Fexample.com");
await expect(page.getByRole("heading", { name: "Log in to Densio" })).toBeVisible();
await expect(page.locator('input[name="returnTo"]')).toHaveValue("/app");
});

test("A canceled checkout offers a working route back to the account", async ({ page }) => {
const response = await page.goto("/checkout/canceled");
expect(response?.status()).toBe(200);
await expect(page.getByRole("heading", { name: "Checkout canceled" })).toBeVisible();
await page.getByRole("link", { name: "Go to billing", exact: true }).click();
await expect(page).toHaveURL(/\/auth\/login\?returnTo=%2Fapp$/);
await expect(page.getByRole("heading", { name: "Log in to Densio" })).toBeVisible();
});
18 changes: 18 additions & 0 deletions e2e/pulse/legal.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { expect, test } from "@playwright/test";

const pages = [
{ name: "Privacy Policy", path: "/privacy-policy" },
{ name: "Terms & Conditions", path: "/terms" },
];

for (const { name, path } of pages) {
test(`A visitor can read ${name} from the homepage footer`, async ({ page }) => {
await page.goto("/");
const link = page.getByRole("contentinfo").getByRole("link", { name, exact: true });
await expect(link).toHaveAttribute("href", path);
await link.click();
await expect(page).toHaveURL(new RegExp(`${path}$`));
await expect(page.getByRole("heading", { name, exact: true, level: 1 })).toBeVisible();
await expect(page.getByRole("main").getByRole("heading", { level: 2 }).first()).toBeVisible();
});
}
20 changes: 20 additions & 0 deletions e2e/pulse/mobile.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { expect, test } from "@playwright/test";

test.use({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true });

test("Mobile visitors can open the menu and reach email sign-in", async ({ page }) => {
const response = await page.goto("/");
expect(response?.status()).toBe(200);
await expect(page.getByRole("heading", { name: "Agent first video optimization" })).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(
true,
);
await page.getByRole("button", { name: "Open menu", exact: true }).click();
const menu = page.getByRole("dialog", { name: "Menu", exact: true });
await expect(menu).toBeVisible();
await menu.getByRole("link", { name: "Sign in to Densio", exact: true }).click();
await expect(page).toHaveURL(/\/auth\/login$/);
await expect(menu).toHaveCount(0);
await expect(page.getByRole("textbox", { name: "Email", exact: true })).toBeEditable();
await expect(page.getByRole("button", { name: "Continue with email" })).toBeInViewport();
});
25 changes: 25 additions & 0 deletions e2e/pulse/onboarding.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { expect, test } from "@playwright/test";

test("A visitor can copy the agent onboarding prompt", async ({ page, context }) => {
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
const response = await page.goto("/");
expect(response?.status()).toBe(200);
await expect(page.getByRole("heading", { name: "Agent first video optimization" })).toBeVisible();
await page.getByRole("button", { name: "Copy prompt", exact: true }).click();
await expect(page.getByRole("button", { name: "Copied!", exact: true })).toBeVisible();
const prompt = await page.evaluate(() => navigator.clipboard.readText());
expect(prompt).toContain("npx skills add pixel-point/densio --skill densio");
expect(prompt).toContain("ask me which video to optimize");
expect(prompt).toContain("save the compressed files in my project");
});

test("Get Started takes a visitor to email sign-in", async ({ page }) => {
await page.goto("/");
await page.getByRole("link", { name: "Get Started", exact: true }).click();
await expect(page).toHaveURL(/\/auth\/login$/);
await expect(page.getByRole("heading", { name: "Log in to Densio" })).toBeVisible();
await expect(page.getByRole("textbox", { name: "Email", exact: true })).toBeEditable();
await expect(page.getByRole("button", { name: "Continue with email" })).toBeEnabled();
await page.getByRole("link", { name: "Densio home", exact: true }).click();
await expect(page.getByRole("heading", { name: "Agent first video optimization" })).toBeVisible();
});
5 changes: 5 additions & 0 deletions e2e/pulse/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": { "lib": ["ES2024", "DOM"], "types": ["node"] },
"include": ["*.ts", "../../siteos.playwright.config.ts"]
}
3 changes: 2 additions & 1 deletion e2e/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"compilerOptions": {
"types": ["node", "vitest/globals"]
},
"include": ["**/*.ts", "../apps/api/src/markdown.d.ts"]
"include": ["**/*.ts", "../apps/api/src/markdown.d.ts"],
"exclude": ["pulse"]
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"test:media": "turbo run test:media"
},
"devDependencies": {
"@playwright/test": "1.63.0",
"@types/archiver": "^8.0.0",
"@types/node": "^22.18.0",
"@typescript/native-preview": "^7.0.0-dev.20260703.1",
Expand Down
Loading
Loading