All checks were successful
Quality gates / quality (pull_request) Successful in 1m43s
99 lines
5.2 KiB
JavaScript
99 lines
5.2 KiB
JavaScript
import { chromium } from 'playwright';
|
|
import assert from 'node:assert/strict';
|
|
|
|
const appUrl = new URL(process.env.TIMMY_STAGING_URL || 'http://127.0.0.1:4173/');
|
|
const expectedTag = process.env.TIMMY_EXPECT_RELEASE_TAG || 'daily-test';
|
|
const expectedCommit = process.env.TIMMY_EXPECT_RELEASE_COMMIT || 'c'.repeat(40);
|
|
const username = process.env.TIMMY_STAGING_USER || '';
|
|
const password = process.env.TIMMY_STAGING_PASSWORD || '';
|
|
const loopback = ['127.0.0.1', 'localhost', '::1'].includes(appUrl.hostname);
|
|
if (!loopback && (!username || !password)) throw new Error('Live staging smoke requires TIMMY_STAGING_USER and TIMMY_STAGING_PASSWORD.');
|
|
if (!/^[0-9a-f]{40}$/.test(expectedCommit)) throw new Error('TIMMY_EXPECT_RELEASE_COMMIT must be a full lowercase commit.');
|
|
|
|
const browser = await chromium.launch({ headless: true });
|
|
try {
|
|
const context = await browser.newContext({
|
|
viewport: { width: 390, height: 844 },
|
|
deviceScaleFactor: 2,
|
|
serviceWorkers: 'block',
|
|
acceptDownloads: true,
|
|
...(username && password ? { httpCredentials: { username, password } } : {}),
|
|
});
|
|
const page = await context.newPage();
|
|
const browserErrors = [];
|
|
const secretFindings = [];
|
|
let agentChatRequests = 0;
|
|
page.on('console', message => { if (message.type() === 'error') browserErrors.push(message.text()); });
|
|
page.on('pageerror', error => browserErrors.push(error.message));
|
|
page.on('request', request => { if (new URL(request.url()).pathname.endsWith('/api/agent/chat')) agentChatRequests += 1; });
|
|
page.on('response', async response => {
|
|
const url = new URL(response.url());
|
|
if (!url.pathname.includes('/api/')) return;
|
|
for (const name of Object.keys(response.headers())) {
|
|
if (/authorization|set-cookie|x-api-key/i.test(name)) secretFindings.push(`sensitive response header ${name}`);
|
|
}
|
|
try {
|
|
const type = response.headers()['content-type'] || '';
|
|
if (/json|text/.test(type)) {
|
|
const body = (await response.text()).slice(0, 65_537);
|
|
if (body.length > 65_536) secretFindings.push('oversized API response');
|
|
if (/"(?:password|token|cookie|session|api[_-]?key|credential|environment|processEnv)"\s*:/i.test(body)) secretFindings.push(`secret-bearing response ${url.pathname}`);
|
|
}
|
|
} catch {}
|
|
});
|
|
|
|
const navigation = await page.goto(appUrl.toString(), { waitUntil: 'networkidle' });
|
|
assert.equal(navigation?.status(), 200, 'edge authentication and staging navigation must succeed');
|
|
await page.evaluate(() => localStorage.clear());
|
|
await page.reload({ waitUntil: 'networkidle' });
|
|
|
|
const healthUrl = new URL(`${appUrl.pathname.replace(/\/$/, '')}/api/healthz`, appUrl);
|
|
const health = await page.evaluate(async url => {
|
|
const response = await fetch(url, { headers: { accept: 'application/json' } });
|
|
return { status: response.status, body: await response.json() };
|
|
}, healthUrl.toString());
|
|
assert.equal(health.status, 200);
|
|
assert.equal(health.body.ok, true);
|
|
assert.equal(health.body.release, expectedTag);
|
|
assert.equal(health.body.commit, expectedCommit);
|
|
assert.equal(health.body.agentEnabled, false, 'Phase 1 staging must keep Hermes off');
|
|
|
|
assert.match(await page.locator('.staging-label').innerText(), new RegExp(`Staging · ${expectedTag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')} · ${expectedCommit.slice(0, 12)}`));
|
|
assert.equal(await page.locator('main .btn-primary:visible').count(), 1, 'exactly one dominant photo action');
|
|
assert.equal(await page.locator('[data-log]:visible').count(), 1, 'manual fallback remains visible');
|
|
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true, 'home must not overflow');
|
|
|
|
await page.locator('[data-log]:visible').click();
|
|
await page.locator('[data-type="4"]').click();
|
|
await page.locator('#next').click();
|
|
await page.locator('#note').fill('synthetic staging smoke');
|
|
await page.locator('#next').click();
|
|
await page.locator('#save').click();
|
|
|
|
await page.getByRole('button', { name: 'Journal', exact: true }).click();
|
|
assert.match(await page.locator('main').innerText(), /synthetic staging smoke/);
|
|
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true, 'journal must not overflow');
|
|
|
|
await page.getByRole('button', { name: 'Timmy', exact: true }).click();
|
|
await page.locator('#chat-message').fill('I barfed');
|
|
await page.locator('#send-chat').click();
|
|
await page.waitForSelector('.bubble.timmy >> text=Pause and get medical help');
|
|
assert.equal(agentChatRequests, 0, 'urgent phrase must be intercepted before Hermes');
|
|
|
|
await page.getByRole('button', { name: 'Journal', exact: true }).click();
|
|
await page.locator('[data-view="privacy"]').click();
|
|
const downloadPromise = page.waitForEvent('download');
|
|
await page.locator('#export').click();
|
|
const download = await downloadPromise;
|
|
assert.equal(download.suggestedFilename(), 'timmy-ledger.json');
|
|
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true, 'privacy screen must not overflow');
|
|
|
|
await page.waitForTimeout(100);
|
|
assert.deepEqual(secretFindings, []);
|
|
assert.deepEqual(browserErrors, []);
|
|
await context.close();
|
|
console.log(`PASS staging smoke ${expectedTag} ${expectedCommit.slice(0, 12)} at ${appUrl}`);
|
|
} finally {
|
|
await browser.close();
|
|
}
|