Mercurial
view hg-web/e2e/app_e2e_test.js @ 271:13d61401c57d
redirect Copilot cache to service state
Set XDG_CACHE_HOME from the configured inference state so the systemd service can extract Copilot outside its protected home directory.
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Fri, 07 Aug 2026 13:24:05 -0700 |
| parents | c5129452493e |
| children |
line wrap: on
line source
const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const { spawn, spawnSync } = require('node:child_process'); const { chromium } = require('playwright-core'); const BASE_URL = 'http://127.0.0.1:6970'; const RUNFILES = process.env.JS_BINARY__RUNFILES; const WORKSPACE = process.env.JS_BINARY__WORKSPACE; let hgCommand = 'hg'; function run(command, args, options = {}) { const result = spawnSync(command, args, { encoding: 'utf8', ...options, }); if (result.status !== 0) { throw new Error( `${command} ${args.join(' ')} failed\n${result.stdout || ''}${result.stderr || ''}`, ); } return result.stdout.trim(); } function mercurialEnvironment(home) { return { ...process.env, HGPLAIN: '1', HGRCPATH: '', HOME: home, }; } function stopProcess(child) { if (!child || child.exitCode !== null) return Promise.resolve(); child.kill('SIGTERM'); return new Promise(resolve => { const timer = setTimeout(() => { if (child.exitCode === null) child.kill('SIGKILL'); }, 3000); child.once('exit', () => { clearTimeout(timer); resolve(); }); }); } async function waitForHttp(url, child, logs) { const deadline = Date.now() + 15000; while (Date.now() < deadline) { if (child.exitCode !== null) { throw new Error(`Server exited early with ${child.exitCode}\n${logs.join('')}`); } try { const response = await fetch(url); if (response.ok) return; } catch { // Keep waiting for startup. } await new Promise(resolve => setTimeout(resolve, 100)); } throw new Error(`Timed out waiting for ${url}\n${logs.join('')}`); } function createFixtureRepository(root) { const options = { env: mercurialEnvironment(root) }; run(hgCommand, ['init', root], options); fs.mkdirSync(path.join(root, 'docs'), { recursive: true }); fs.mkdirSync(path.join(root, 'src'), { recursive: true }); fs.writeFileSync( path.join(root, 'README.md'), '# Fixture Repository\n\n<script>window.__hgWebXss = true</script>\n', ); fs.writeFileSync(path.join(root, 'docs', 'README.md'), '# Documentation\n\nNested README.\n'); fs.writeFileSync(path.join(root, 'src', 'main.c'), 'int main(void) { return 0; }\n'); fs.writeFileSync( path.join(root, 'BUILD'), 'cc_library(\n name = "fixture",\n srcs = ["src/main.c"],\n)\n', ); fs.writeFileSync( path.join(root, 'pixel.png'), Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl2nWQAAAAASUVORK5CYII=', 'base64', ), ); fs.writeFileSync(path.join(root, 'movie.mp4'), Buffer.from('00000018667479706d703432', 'hex')); fs.writeFileSync(path.join(root, 'sound.mp3'), Buffer.from('ID3')); fs.writeFileSync(path.join(root, 'document.pdf'), Buffer.from('%PDF-1.4\n%%EOF\n')); fs.writeFileSync(path.join(root, 'archive.bin'), Buffer.from([0, 1, 2, 3])); run(hgCommand, ['--repository', root, 'add'], options); run(hgCommand, ['--repository', root, 'commit', '-u', 'Test User <[email protected]>', '-m', 'Initial fixture'], options); const firstNode = run(hgCommand, ['--repository', root, 'log', '-r', '.', '-T', '{node}'], options); fs.writeFileSync( path.join(root, 'README.md'), '# Updated Fixture Repository\n\n<script>window.__hgWebXss = true</script>\n\nSecond revision.\n', ); fs.writeFileSync(path.join(root, 'new-file.txt'), 'new file\n'); run(hgCommand, ['add', 'new-file.txt'], { ...options, cwd: root }); run(hgCommand, ['--repository', root, 'commit', '-u', 'Test User <[email protected]>', '-m', 'Update fixture'], options); const tipNode = run(hgCommand, ['--repository', root, 'log', '-r', '.', '-T', '{node}'], options); return { firstNode, tipNode }; } async function assertJson(pathname, status = 200) { const response = await fetch(`${BASE_URL}${pathname}`); const body = await response.text(); assert.equal(response.status, status, `${pathname} status: ${body}`); return JSON.parse(body); } async function assertPageHasNoBrowserErrors(browser, pathname, assertion) { const page = await browser.newPage(); const errors = []; page.on('pageerror', error => errors.push(`pageerror: ${error.stack || error.message}`)); page.on('console', message => { if (message.type() === 'error') errors.push(`console: ${message.text()}`); }); page.on('requestfailed', request => { errors.push(`requestfailed: ${request.url()} ${request.failure()?.errorText || ''}`); }); page.on('response', response => { if (response.status() >= 400) { errors.push(`response: ${response.status()} ${response.url()}`); } }); const response = await page.goto(`${BASE_URL}${pathname}`, { waitUntil: 'networkidle' }); assert.equal(response.status(), 200, `${pathname} document status`); await assertion(page); if (errors.length > 0) { throw new Error(`${pathname} browser errors\n${errors.join('\n')}`); } await page.close(); } async function main() { assert.ok(RUNFILES, 'rules_js runfiles path is required'); assert.ok(WORKSPACE, 'rules_js workspace name is required'); const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'hg-web-e2e-')); const appLogs = []; const hgLogs = []; let hgServer; let appServer; let browser; try { const runfilesWorkspace = path.join(RUNFILES, WORKSPACE); const serverBinary = path.join(runfilesWorkspace, 'hg-web', 'hg_web_server'); hgCommand = path.join( runfilesWorkspace, 'third_party', 'mercurial', 'runtime', 'bin', 'hg', ); const chromiumPath = path.resolve(process.env.CHROMIUM_PATH); assert.ok(fs.existsSync(serverBinary), `missing server binary: ${serverBinary}`); assert.ok(fs.existsSync(hgCommand), `missing bundled Mercurial: ${hgCommand}`); assert.ok(fs.existsSync(chromiumPath), `missing Chromium binary: ${chromiumPath}`); const { firstNode, tipNode } = createFixtureRepository(fixtureRoot); hgServer = spawn( hgCommand, [ '--repository', fixtureRoot, 'serve', '--address', '127.0.0.1', '--port', '4444', '--accesslog', '-', '--errorlog', '-', ], { env: mercurialEnvironment(fixtureRoot), stdio: ['ignore', 'pipe', 'pipe'], }, ); hgServer.stdout.on('data', chunk => hgLogs.push(chunk.toString())); hgServer.stderr.on('data', chunk => hgLogs.push(chunk.toString())); appServer = spawn(serverBinary, [], { cwd: runfilesWorkspace, stdio: ['ignore', 'pipe', 'pipe'], }); appServer.stdout.on('data', chunk => appLogs.push(chunk.toString())); appServer.stderr.on('data', chunk => appLogs.push(chunk.toString())); await waitForHttp(`${BASE_URL}/`, appServer, appLogs); if (hgServer.exitCode !== null) { throw new Error(`Mercurial server exited early with ${hgServer.exitCode}\n${hgLogs.join('')}`); } await waitForHttp(`${BASE_URL}/api/repo/list`, hgServer, hgLogs); for (const pathname of ['/', '/directory', '/directory?path=docs', '/graph', `/changeset/${tipNode}`]) { const response = await fetch(`${BASE_URL}${pathname}`); assert.equal(response.status, 200, `${pathname} shell route`); assert.match(await response.text(), /<title>Zenbu Repository<\/title>/); } for (const pathname of ['/page.js', '/index.css', '/base.css', '/pencil_lines.png', '/panda.png']) { const response = await fetch(`${BASE_URL}${pathname}`); assert.equal(response.status, 200, `${pathname} static asset`); assert.ok((await response.arrayBuffer()).byteLength > 0, `${pathname} is non-empty`); } for (const pathname of ['/hg-web-background.jpg', '/pencil_texture.png']) { assert.equal((await fetch(`${BASE_URL}${pathname}`)).status, 404); } const rootList = await assertJson('/api/repo/list'); assert.ok(rootList.directories.some(entry => entry.basename === 'docs')); assert.ok(rootList.files.some(entry => entry.basename === 'README.md')); const nestedList = await assertJson('/api/repo/list?path=docs'); assert.ok(nestedList.files.some(entry => entry.basename === 'README.md')); const fileResponse = await fetch(`${BASE_URL}/api/repo/file?path=src%2Fmain.c`); assert.equal(fileResponse.status, 200); assert.match(await fileResponse.text(), /int main/); for (const [filename, contentType, disposition] of [ ['pixel.png', 'image/png', 'inline'], ['movie.mp4', 'video/mp4', 'inline'], ['sound.mp3', 'audio/mpeg', 'inline'], ['document.pdf', 'application/pdf', 'inline'], ['archive.bin', 'application/octet-stream', 'attachment'], ]) { const response = await fetch( `${BASE_URL}/api/repo/file?path=${encodeURIComponent(filename)}`, ); assert.equal(response.status, 200, `${filename} response`); assert.match(response.headers.get('content-type') || '', new RegExp(`^${contentType}`)); assert.equal(response.headers.get('content-disposition'), disposition); assert.equal(response.headers.get('x-content-type-options'), 'nosniff'); assert.ok((await response.arrayBuffer()).byteLength > 0); } const binaryResponse = await fetch(`${BASE_URL}/api/repo/file?path=archive.bin`); assert.deepEqual( Buffer.from(await binaryResponse.arrayBuffer()), Buffer.from([0, 1, 2, 3]), ); const readmeResponse = await fetch(`${BASE_URL}/api/repo/readme?path=docs`); assert.equal(readmeResponse.status, 200); assert.match(await readmeResponse.text(), /Nested README/); assert.equal((await fetch(`${BASE_URL}/api/repo/readme?path=src`)).status, 204); const graph = await assertJson('/api/graph/tip?style=json'); assert.equal(graph.node, tipNode); assert.ok(graph.changesets.length >= 2); const changeset = await assertJson(`/api/changeset/${tipNode}`); assert.equal(changeset.node, tipNode); assert.equal(changeset.desc, 'Update fixture'); assert.ok(changeset.files.some(entry => entry.file === 'new-file.txt' && entry.status === 'added')); assert.ok(changeset.diff.length > 0); for (const pathname of [ '/api/repo/list?path=..%2Fetc', '/api/repo/file?path=..%2FREADME.md', '/api/graph/not-a-node?style=json', '/api/changeset/not-a-node', ]) { const response = await fetch(`${BASE_URL}${pathname}`); assert.equal(response.status, 400, `${pathname} rejects invalid input`); } assert.equal((await fetch(`${BASE_URL}/missing-route`)).status, 404); const identify = run( hgCommand, ['identify', `${BASE_URL}/repo`], { env: mercurialEnvironment(fixtureRoot) }, ); assert.match(identify, new RegExp(`^${tipNode.slice(0, 12)}`)); browser = await chromium.launch({ executablePath: chromiumPath, headless: true, args: ['--no-sandbox'], }); await assertPageHasNoBrowserErrors(browser, '/', async page => { await page.getByRole('heading', { name: 'Zenbu Repository' }).waitFor(); await page.getByText('Recent Commits').waitFor(); await page.getByText('Repository Files').waitFor(); assert.equal(await page.evaluate(() => window.__hgWebXss), undefined); assert.equal( await page.locator('.graph-container').evaluate( element => getComputedStyle(element).backgroundImage, ), 'none', ); await page.locator('.theme-toggle').click(); }); await assertPageHasNoBrowserErrors(browser, '/directory', async page => { await page.getByText('Repository Files').waitFor(); await page.getByRole('link', { name: 'README.md' }).first().click(); await page.getByText('Updated Fixture Repository').waitFor(); assert.equal(await page.evaluate(() => window.__hgWebXss), undefined); await page.keyboard.press('Escape'); let delayedReadmeRequested = false; let markReadmeRequested; const readmeRequested = new Promise(resolve => { markReadmeRequested = resolve; }); await page.route(/\/api\/repo\/readme\?path=/, async route => { delayedReadmeRequested = true; markReadmeRequested(); await new Promise(resolve => setTimeout(resolve, 500)); await route.continue(); }); await page.getByRole('link', { name: 'docs' }).click(); await Promise.race([ readmeRequested, new Promise((_, reject) => { setTimeout(() => reject(new Error('Timed out waiting for delayed README request')), 5000); }), ]); await page.getByRole('link', { name: 'root' }).click(); await page.getByText('Updated Fixture Repository').waitFor(); await page.waitForTimeout(600); assert.equal(await page.getByText('Documentation').count(), 0); assert.equal(delayedReadmeRequested, true); await page.getByRole('link', { name: 'pixel.png' }).click(); const image = page.locator('.static-file-image'); await image.waitFor(); await image.evaluate(element => { const imageElement = element; if (imageElement.complete) return; return new Promise((resolve, reject) => { imageElement.addEventListener('load', resolve, { once: true }); imageElement.addEventListener('error', reject, { once: true }); }); }); assert.equal(await image.evaluate(element => element.naturalWidth), 1); await page.getByRole('dialog', { name: 'Preview pixel.png' }).waitFor(); await page.keyboard.press('Escape'); await page.getByRole('link', { name: 'BUILD' }).click(); const buildCode = page.locator('code.language-python'); await buildCode.waitFor(); await page.getByText('cc_library').waitFor(); assert.match(await buildCode.textContent(), /name = "fixture"/); await page.keyboard.press('Escape'); await page.getByRole('link', { name: 'src' }).click(); await page.getByRole('link', { name: 'main.c' }).click(); await page.getByText('int main(void)').waitFor(); await page.keyboard.press('Escape'); }); await assertPageHasNoBrowserErrors(browser, '/directory?path=docs', async page => { await page.getByText('Documentation').waitFor(); await page.getByRole('link', { name: 'README.md' }).click(); await page.getByText('Nested README.').waitFor(); }); await assertPageHasNoBrowserErrors(browser, '/graph', async page => { await page.getByText('Commit Graph').waitFor(); await page.waitForFunction(() => new URL(window.location.href).searchParams.has('tip')); const graphUrl = page.url(); const rows = page.locator('.graph-row'); await rows.first().click(); await page.waitForURL(/\/changeset\/[0-9a-f]+$/); await page.getByRole('heading', { name: 'Update fixture' }).waitFor(); await page.getByRole('button', { name: 'Back', exact: true }).click(); await page.waitForFunction(expected => window.location.href === expected, graphUrl); await page.getByText('Commit Graph').waitFor(); }); await assertPageHasNoBrowserErrors(browser, `/changeset/${tipNode}`, async page => { await page.getByRole('heading', { name: 'Update fixture' }).waitFor(); await page.locator('.changeset-files code').filter({ hasText: 'new-file.txt' }).waitFor(); await page.getByText('added', { exact: true }).waitFor(); await page.getByRole('region', { name: 'Changeset diff' }).waitFor(); await page.locator('.diff-column-headings').getByText('Before').first().waitFor(); await page.locator('.diff-column-headings').getByText('After').first().waitFor(); await page.locator('.diff-left.diff-remove').filter({ hasText: '# Fixture Repository' }).waitFor(); await page.locator('.diff-right.diff-add').filter({ hasText: '# Updated Fixture Repository' }).waitFor(); assert.equal( await page.locator('.diff-left.diff-context').filter({ hasText: '<script>' }).first().textContent(), '<script>window.__hgWebXss = true</script>', ); assert.equal( await page.locator('.changeset-paper').evaluate( element => getComputedStyle(element).backgroundImage, ), 'none', ); await page.getByRole('button', { name: firstNode.slice(0, 12) }).click(); await page.waitForFunction( expectedPath => window.location.pathname === expectedPath, `/changeset/${firstNode}`, ); await page.getByRole('heading', { name: 'Initial fixture' }).waitFor(); await page.getByRole('button', { name: 'Back', exact: true }).click(); await page.getByRole('heading', { name: 'Update fixture' }).waitFor(); await page.getByRole('button', { name: 'Back', exact: true }).click(); await page.getByText('Commit Graph').waitFor(); }); await assertPageHasNoBrowserErrors(browser, '/changeset/not-a-node', async page => { await page.getByText('Recent Commits').waitFor(); }); await stopProcess(hgServer); const unavailable = await fetch(`${BASE_URL}/api/repo/list`); assert.equal(unavailable.status, 502); assert.equal((await fetch(`${BASE_URL}/`)).status, 200); } finally { if (browser) await browser.close(); await stopProcess(appServer); await stopProcess(hgServer); fs.rmSync(fixtureRoot, { recursive: true, force: true }); } } main().catch(error => { console.error(error.stack || error); process.exitCode = 1; });