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