timmy-talking-turd/tests/staging.acceptance.mjs
Timmy d576a74b96
All checks were successful
Quality gates / quality (pull_request) Successful in 1m43s
feat: add hardened atomic staging deployment
2026-08-21 14:32:31 +00:00

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();
}